Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 21:24
· 364 commits to main since this release

Added

  • validate_pvs can answer the other question about a display: view="file" | "display". The
    tool has always reported what a .bob file 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 answered total: 0 while
    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 answered total: 0 under 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_path result now carries shown_by_display and shown_by_display_capped, and
    under view="file" a notes entry 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 the Fixed entry 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; --list describes them) and then runs
    epics-doctor against exactly that block, so generating a configuration and checking it are one
    step. The block goes to stdout and everything else to stderr, so epics-init --preset X > .mcp.json yields a usable file. --set NAME=VALUE fills or adds a variable, --probe-pv NAME is
    passed to the doctor, --no-check emits only. It introduces no new EPICS_MCP_* variable: the
    presets set existing ones. Exit codes are the doctor's own (0/1/3), with 2 for 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-pv is reported as
    confirming nothing, because the live plane makes no network call in that case; and sandbox
    searches BOTH ways a PVA client can, UDP broadcast to 127.0.0.1 and 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 the epics-init command to run.

  • monitor_pv says whether the channel was reachable. A new connection field
    (connected / disconnected / unknown) travels with the events, and connection_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", and get_pv_value contradicted this
    tool on the second case by raising PV_TIMEOUT, so the two disagreed about one fact.
    connected is claimed only where a delivered value proves it; a stream the server ended, or
    one that reported an error, is unknown rather than a guess. A subscription RemoteError now
    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 and list_log_attachments now carry title,
    description, owner (the author), source, properties and the raw attachments list for every
    server, and download_log_attachment hands 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 in docs/safety.md; if a real
    facility privacy specification ever arrives, it will be rebuilt against that specification. With
    it go, user-visibly: the env vars EPICS_MCP_OLOG_ASSUME_TEST_DATA and
    EPICS_MCP_OLOG_ALLOW_ATTACHMENT_DOWNLOAD (now unknown, the config warns if still set), the
    error codes OLOG_WHOLE_MODE_REQUIRED and OLOG_ATTACHMENT_DOWNLOAD_DENIED, the withheld
    fields of the download/list results, and the doctor's Olog free-text: line together with the
    olog_freetext_withheld field its privacy report carried, which epics-doctor --json therefore
    no longer emits. The whole-mode preconditions of add_log_attachment / update_log_entry are
    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_configured returns the authored
    fields (description/guidance/displays/commands/actions and the serialized config_msg)
    with their values, exactly the handling instruction the withholding used to blank, and
    get_alarm_history events carry user/host/command/config_msg again. The known-field
    allowlists stay as structure: an unknown field a future logger version adds is still dropped.

Changed

  • validate_pvs refuses an unusable file_path at once instead of walking the whole dataset
    first.
    A path that is not a .bob, or that lies outside displays_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 .bob files 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_names for a plain list of PVs).
    A client that passed a non-.bob path today received a successful total: 0, and will now
    get an error.
    The suffix comparison folds case, so UPPER.BOB is still a display; and a
    genuine .bob that declares no real ca/pva channels is unchanged, that stays an honest
    total: 0, not a refusal. Passing pv_names as well makes the list win, and the file path is
    neither read nor refused.
  • coverage_audit reports a missing alarm tree before doing the work, not after. Asking for
    the alarm plane without naming a tree (alarm_config) was already an INVALID_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 the validate_pvs one above, in the
    sibling tool. The error itself is unchanged.
  • The compare_machine_state prompt no longer suggests validate_pvs for a non-display file.
    With a reference_file such 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
    .bob reference 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_results and the context_cap of
    crossplane_check, coverage_audit and find_device require >= 1, and monitor_pv.duration
    requires > 0. A cap of 0 did not fail before: it succeeded and returned nothing, which a
    client cannot tell from "the thing you asked about does not exist". find_channels was the
    sharpest case, returning an empty channel list together with capped: true, so the answer
    claimed there was more while showing none. A client passing 0 today 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 timeout arguments 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_pvs and find_device require > 0, which the other nineteen timeouts already did.
    A client passing timeout=0 today 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_device answered "No operator-facing screen references
    this device", validate_pvs reported the PV as disconnected, diagnose_connection named a
    cause, discover_pvs and get_pvs came back empty. Two of the five that did raise pointed at
    the wrong thing: PV_TIMEOUT blames the device rather than the argument, and is_archived
    surfaced INTERNAL with 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 a number it requires an EXCLUSIVE lower bound, so a future ge=0 cannot reintroduce
    the same defect.
  • epics-crossplane --help and epics-coverage --help now answer on a core-only install. They
    used to report the missing display engine and exit 2 instead, so on any install from a package
    index, where that engine is never present, the first answer to --help was 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 --version answers 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 --nope on 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 is 2 where the engine is absent, as before, and 1 where 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/ and CLAUDE.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_pvs answered total: 0 without 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 answering total: 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, exact total: 0, because calling that a lower bound would be a false statement. No file
    that carried the note before loses it. The view="display" verdict is unchanged.
  • The shipped status legend explained 9 of the 12 states epics-doctor can print, and two of its
    marks not at all.
    disabled, info and disconnected were never named by their status name
    anywhere in the guide, and the marks · and i had 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 against PlaneStatus: a state that is
    not documented is a failing test rather than a decision. Still not documented, and now recorded as
    such: the Overall: 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-doctor is
    grouped by check and prints seven, because the Archiver's management root and its retrieval root
    are separate planes. archiver_retrieval therefore 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 --json field carries them.
  • The guide's advice for sizing EPICS_MCP_READ_RATE_LIMIT was wrong for one of the two tools it
    named, and unusable for the other.
    It said a multi-GET tool such as coverage_audit or
    crossplane_check spends "several tokens per audited PV". Measured at the throttle itself,
    crossplane_check is 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_audit is 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. scan pads 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.md section 10.
  • Two guards hold the shipped guide's new plane inventory: the region against a real run_doctor
    run, and the plane-name literals in services/doctor.py against 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 in docs/known-limits.md
    section 14.