Skip to content

Releases: epicDirk/EPICS-MCP

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 24 Aug 11:11

Changed

  • BREAKING: the three MCP resources moved from epics-pv:// to epics://.
    epics-pv://health, epics-pv://config and epics-pv://guide are now epics://health,
    epics://config and epics://guide. Are you affected? Search your client config, your
    scripts and your prompts for epics-pv://. No hit: nothing to do, because a client that
    discovers resources through resources/list picks the new names up by itself. A hit: change
    the scheme there. There is no alias period, and an old URI fails loudly rather than answering
    something stale. The rename itself changed no payload, but two of the three changed elsewhere
    in this release: epics://health now reports server as epics-pv instead of epics-mcp,
    and the guide grew. epics://config is unchanged. Wherever an entry describes what an EARLIER
    version served, here or in the released sections below, it keeps writing epics-pv://: those
    sentences record the address of the day, so your search will hit them and they are not work.

  • BREAKING, but only if you set EPICS_MCP_READ_RATE_LIMIT: epics-doctor now exits 3
    where it exited 0.
    The limit is off by default, and a run that is denied nothing is
    unchanged in every field. Are you affected? Check whether that variable is set, above 0,
    in the environment your epics-doctor job runs in. Setting it is necessary for this change and
    not sufficient: the exit moves only on a run whose own reads the throttle actually refused, so
    the variable can stand for months and nothing move. No 0.6.0 output warns you in advance,
    either way. Until now a run whose own reads that limit refused could still
    exit 0; it cannot any more, because a refused read means part of the run was never measured.
    After upgrading, the new » lines name each plane whose probe the throttle refused.
    What to do: if a CI job asserts exit 0 and goes red, raise the limit, or set it to 0 to
    switch the throttle off, rather than lowering the assertion. The red is the finding: the 0 it
    replaces was printed for a run that had not looked. Exit codes are 0 clean, 1 failed, 2
    usage, 3 inconclusive; 3 is not new, it already covered a failed identity probe in 0.6.0.

  • find_device no longer calls a Data Browser trend an operator screen. A .plt opened by a
    button is operator-facing, so the reverse-lookup returns it, and until now it arrived
    indistinguishable from a .bob: counted among the screens, described as one by the tool, and on
    an empty answer the note read "No operator-facing screen references this device/query" although
    trends had never been counted apart. Each match now carries screens[].node_kind ("display"
    or "trend"), and the report carries display_count and trend_count, counted positively
    rather than one subtracted from the other, so a later third kind cannot land silently in the
    display figure. The rendered header splits the same way and a trend's own row says what it is;
    the empty answer denies both kinds. Additive on the wire: nothing was removed or retyped, and
    the field stays called screens, because renaming it would break every caller in order to say
    what its members now say themselves. The three cap notes follow: they qualified "a screen" and
    "the screen list" where a trend can be affected too, and now say "a screen or trend" and "the
    match list". The description growth lands on the display-gated lane only; the core lane's
    tools/list is unchanged.

  • epics-doctor no longer blames your services for a limit you set on it. With
    EPICS_MCP_READ_RATE_LIMIT set tight enough to refuse the doctor's own reads, the report said
    four different untrue things about a stack that was up. A refused TRANSPORT probe was
    unreachable (exit 1), telling you to check a host and port nothing had contacted; a refused
    IDENTITY beacon was identity_probe_failed, pointing at a sub-path and an auth wall for a
    request never sent; the cross-plane block then reported the host as dead and offered a swapped
    archiver URL pair; and one read below the budget of a full run the refusal landed on the
    archiver's ingest sub-probe, leaving every plane , every list empty and exit 0 under this
    tool's strongest confirmation. A plane this command never asked is now throttled (»), listed
    in throttled_planes and NOT in inconclusive_identity_planes, with a remedy naming the limit
    instead of a URL. A run denied any read reports reads_denied, closes verification_complete
    and exits 3, which is the only signal for the sub-probe case. Scripts reading the exit code
    alone see a 0 become a 3 on a deployment whose configuration did not change.

  • An unwritable audit path no longer hands its full local path to a tool caller, at EITHER write
    gate.
    Both refuse with SAFETY_CONFIG_INVALID naming EPICS_MCP_AUDIT_LOG_FILE and withholding
    its value; the path stays in the exception's details, which the tool boundary does not send.
    Each gate is built on first use unless its own write flag is on, so the refusal is a tool ANSWER
    rather than a start-up failure: on the Olog side from the four write tools, and on the PV side
    from set_pv_value, which builds its gate BEFORE it checks whether writes are allowed, so this
    reached callers even with PV writes off. Operators reading a start-up failure on stderr now see
    the variable name instead of its value
    and read the value back from the environment of the
    process they started. Which case arrives how: operator_guide.md under SAFETY_CONFIG_INVALID,
    and docs/safety.md for the posture and the measurement behind it.

  • The server now names itself epics-pv, the key a client registers it under, in both places it
    answers "which server am I".
    serverInfo on the handshake and the server field of
    epics-pv://health both said epics-mcp, which is the DISTRIBUTION name: the package on PyPI
    and the console script keep it, and neither changes. Nothing hangs behaviour on either value (the
    MCP specification reserves serverInfo for display, logging and debugging), so this costs a
    reader nothing except the wrong trail it used to lay when two of the three servers in one client
    log named themselves after something else.

  • find_device now says that every match comes back with the roles it uses the device in.
    The answer has carried screens[].roles all along and nothing on the surface said so, so
    "which screens can WRITE this device?" was answerable and unadvertised. Nothing changed on the
    wire beyond the sentence.

  • get_pvs now names the field the engineering unit rides in. Its description referred to
    get_pv_info for the metadata, so a caller asking whether channels labelled alike carry the same
    unit had to take a second hop to learn that one batch call answers it. units and precision
    are now named where they sit, in display, per PV. Nothing changed on the wire beyond the
    sentence.

  • set_pv_value now says that an operator screen writes through its widget, and docs/tools.md
    that no preset arms a write gate.
    Both were facts a caller needed before the call and neither
    was anywhere the server delivers: an assistant asked to change a value visible on a screen
    reached for the gated tool and was refused, and sandbox, the one shape with nothing left to
    fill in, reads like the one where writing is on. Nothing changed on the wire beyond the two
    sentences, and this change leaves the initialize instructions untouched (a separate entry in
    this release does edit them).

  • The diagnose_pv prompt now establishes WHICH world it read from, and corroborates a negative
    before concluding.
    The chain started at get_pv_info and ended at the monitor, which taught a
    plan that could reach a confident answer without ever saying which plane had answered it. Step 1
    now says to read the reach field of that first answer before interpreting anything else, and a
    new final step counter-checks with find_channels, get_pv_history and search_logbook,
    because a PV that does not connect looks the same whether it does not exist, is down, or is
    simply out of reach from here; a negative from one plane is not a finding until a second plane
    agrees. Every tool the prompt names is registered in every install, deliberately: an earlier
    draft named the display-gated find_device, which on a core-only install would have been an
    instruction the client cannot follow. This is prompts/list, a surface a client shows without
    any tool call. setup_epics_mcp gains the throttle marker for the read rate limit as a fourth
    finding to read out of an epics-doctor report.

  • The server's initialize instructions name all six Olog write-gate conditions. The
    enumeration in the handshake header listed five and left out the named target logbook, so a
    reader counting the gate from the header got a narrower picture than the operator guide, which
    said six all along. Nothing about the gate itself changed; what changed is that the two agree.

  • diagnose_connection's naming plane runs the same probe as lookup_device_name. Both used
    to carry their own copy of the three-step lookup (config gate, reachability first, then
    validate), which is how two answers to one question start to disagree. The gatherer calls the
    shared query now, like its three sibling planes already did. Semantics are unchanged, including
    that an unreachable service is withheld rather than read as "not registered". On the wire,
    evidence.naming.note reads Naming lookup withheld: ... where it read Naming error: ....
    The older wording survives on one path and deliberately so: the gatherer keeps a wide except
    for the failures the shared query does not catch, and a note from there still reads
    Naming error: ....

  • **epics-init names the variables from your own shell that reac...

Read more

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 03:02

Added

  • epics-pv://health answers the posture questions an approver asks after the write gates.
    Four additions, every one a boolean or a count and none of them an address: rest_tls (whether
    the REST planes verify certificates, whether a CA bundle is configured, and whether any plane
    speaks https at all, since verification is on by default and says nothing where there is no
    certificate to verify),
    rest_read_rate_limit (the opt-in REST GET throttle), allowed_roots_set (whether the opt-in
    file boundary holds a root) and channelfinder_redaction (how many ChannelFinder owner accounts
    and property names the redaction discloses, and whether each allowlist came from the site or from
    the built-in default). Each is named for what it MEASURES rather than for the question that
    brings a reader to it, and three of them needed that. rest_tls.verification_enabled resolves
    the precedence instead of mirroring EPICS_MCP_TLS_VERIFY, because EPICS_MCP_CA_BUNDLE wins
    over that switch: a server with the switch off and a bundle set does verify, and a field
    mirroring the switch would have reported it as unverified. The throttle carries its rest_
    prefix because a p4p PV read runs past it, so "reads are limited" without the prefix would be an
    all-clear for the reads that load an IOC. And the redaction counters say DISCLOSED, because an
    allowlist is the set of what passes through: zero is the most private posture, not a broken one.
    allowed_roots_set is decided by the same predicate the boundary itself asks, so a value of ;
    or of blanks, which resolves to no root at all, reports false rather than claiming a boundary
    that no file argument is held to. Deliberately absent, because a client keeps this payload: the
    CA-bundle path, the roots, and the allowlist entries. epics-doctor prints the allowlist
    entries; the bundle path and the roots are printed by no surface at all and stay in the
    environment the server was started with.

  • epics-testpv, a seventh command: a test PV without a control system. It serves
    TEST:Temperature, an analogue reading with a unit, and TEST:Heater, a writable switch, over
    PVAccess until Ctrl-C. The quick start previously began with softIocPVA, which ships with EPICS
    Base and which no page told you how to obtain, so the promise of a working PV without a facility
    was not keepable. This one needs nothing beyond the install, since p4p is already a dependency.
    It binds loopback unless --interface says otherwise, because a PVA server is a network
    service and its switch accepts writes, and it prints the port it actually bound, which is not the
    default one when that is already taken.

  • epics-init --out PATH writes the configuration file itself, with --force to replace an
    existing one. A shell redirect cannot promise an encoding, and this block is JSON a client has to
    parse: in Windows PowerShell 5.1, > writes UTF-16 with a byte-order mark and
    Set-Content -Encoding utf8 writes UTF-8 with one, and a strict parser rejects both. --out
    writes UTF-8 with LF on every platform. It refuses an existing file by default, because a client
    configuration usually holds other servers, and it writes nothing at all while placeholders remain,
    so filling them in and running again still works.

  • epics-init --absolute-command puts the resolved path of the installed server into the block
    instead of a bare command name. A client launched from a desktop icon does not inherit your
    shell's PATH, which is the commonest reason a correct-looking configuration reports only that
    the server did not start. An unresolvable command is now an error rather than a silent fallback.

  • epics-init warns when the configuration it emits arms a write gate. The check that follows
    prints the resulting posture (see the entry below) but does not evaluate whether such a server
    would START, and with --no-check no check runs at all. The warning also no longer claims that
    the loopback-only search reach is a start condition of both gates: it is one of the PV gate
    alone, while the durable audit path is required by both. It only points at "the check below"
    when one actually follows: the warning is deliberately emitted before the two branches that
    return without running a check (--no-check, and a block with placeholders left in it), which
    are the two cases it exists for, and on both of them it used to promise output that never came.

  • epics-doctor prints what each write gate would allow, and where a write can go. A new
    Write gates block, in the human report and as a write_safety key in --json, covering both
    the PV gate and the Olog logbook gate. A gate that is OFF gets one line saying so; an ARMED one
    adds its allowlist (a PV name pattern, or a set of logbook names), its rate limit, and where a
    write would actually go, which is the EPICS search reach for the PV gate and the target URL for
    the logbook gate. The audit log is named either way, with whether it can be appended to, cannot,
    or could not be decided without creating the file. --json always carries every field.
    Informative: it changes neither the verdict nor the exit code. It reports the
    environment of the command you ran, not necessarily that of a running server, and the heading
    says so. Four states are spelled out rather than left to the reader, because the obvious reading
    of each is its opposite: an empty PV pattern on an armed gate makes the server refuse to start,
    an empty logbook allowlist denies every write, an allowlisted remote target reaches a real
    logbook rather than a sandbox, and an audit verdict that could not be decided is not a "no". The
    audit check opens an append handle and writes nothing; where a file does not exist yet it says
    the answer cannot be determined rather than guessing. A PV pattern that does not COMPILE is named
    as such instead of being shown like a working allowlist, since that too refuses the start; and
    the line about a pattern's width says what was CHECKED, a comparison against a fixed list of
    allow-everything spellings, rather than calling the pattern narrow, because a pattern can admit
    every name and still be written outside that list.

  • Every validate_pvs file-mode answer now names the file it is about. The file_path echo used
    to appear on the empty-result answer only, so one mode came back with two different key sets and a
    client reading both had no stable key to match a result to its call. It now travels with
    shown_by_display and shown_by_display_capped as one group of file-mode fields, present together
    on both file-mode answers or on neither. Passing a NON-EMPTY pv_names still drops all three, and
    that is the point rather than an omission: the list wins, no file is opened, and echoing a path
    there would say the answer came from a file that was never read. An EMPTY list does not win, so
    the file is read as usual and the fields come back. The value is the argument as passed, not the
    resolved path, so it matches what the caller sent; with a symlink the counts describe the target
    while the field names the link. Purely additive: no existing field changes meaning, and the only
    visible difference on the empty-result answer is where the key sits in the object.

  • validate_pvs reports the glob cap, so the absence of a lower-bound note stops meaning
    "complete".
    The display walk has two ways of running out of budget, and this tool only ever
    named one of them. Beside the per-display context cap there is a glob cap: a <file> reference
    that still carries a macro is resolved by globbing the known displays, and past the cap the
    surplus matches are dropped, which removes whole embedded SCREENS rather than instances. It can
    therefore shrink either view while both capped verdicts stay false, and a caller reading the
    quiet answer as complete was reading it wrong. A separate notes entry now names it, worded as a
    statement about the walked dataset rather than about the queried file, because the engine records
    the source display of a capped glob and this tool does not turn that into a per-file verdict.
    coverage_audit and crossplane_check have reported the same signal since they shipped; this
    brings the third display tool in line and adds the regression test coverage_audit never had.
    How much this is worth is measured rather than assumed: on a 2878-display dataset the diagnostic
    holds 16 distinct source/target pairs across 4 source displays, and lifting the cap grows 7
    display views (the largest by 651 channels). On four datasets between 13 and 485 displays it
    never fires at all. Those same figures are why the note names no file: only 3 of the 7 growing
    views appear among the reported sources, so a per-file flag built on that field would miss the
    majority of the damage while looking precise.
    All three notes also stop calling the dropped references "template" ones. Measured against the
    engine, the diagnostic is fed by every glob-resolved <file> reference EXCEPT the template
    ones (embedded, navtabs, open_display and the rule edges), which is the opposite of what the
    wording said; the sentence has been wrong in coverage_audit and crossplane_check since they
    shipped and is corrected in all three.

  • Python 3.14 joins the supported set, and CI tests it. The trove classifiers, which ship
    in both the wheel and the sdist, now advertise 3.12, 3.13 and 3.14, and the CI matrix runs the
    suite on all three rather than on the two it used to. requires-python is unchanged at
    >=3.12, so nothing an installer resolves moves; what changes is that the top of the
    advertised range is now tested rather than merely permitted, which is the gap a classifier
    cannot express on its own.

Fixed

  • A caller could write fabricated records into the audit trail. Both write gates build their
    au...
Read more

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Aug 21:24

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 ...
Read more

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 30 Jul 09:35

Added

  • Every epics-doctor line that reports a problem now names its remedy. Three statuses already
    did (ca_error named the CA bundle variable, api_error the mgmt/retrieval mix-up, config_error
    the variable to set) and four did not: unreachable printed only the transport error,
    backend_down named the consequence rather than the fix, and disconnected and
    identity_probe_failed said nothing about what to do. The remedies now live in one table keyed by
    status, appended to the observation and never replacing it, so --json readers get them in the same
    detail field. An unreachable plane also names the variable it reads its URL from, which is the
    one case where the reader cannot tell from the message which variable to look at. unverified
    and no_ingest deliberately stay without one: the first already carries the specific clue it
    measured, and the second is a fault inside the appliance, not in this configuration.

  • --version on all five console commands (epics-mcp had it; epics-doctor,
    epics-diagnose, epics-coverage and epics-crossplane did not), from one shared helper, so the
    version source and the command-name prefix cannot drift apart. Each prints its own name and the
    package version. Note: on the two display-aware commands the engine check still runs before the
    arguments are parsed, so on a core-only install those two report the missing engine instead; that
    limit is pinned by a test rather than left to be rediscovered.

  • epics-doctor notices an Archiver appliance that is not ingesting. getApplianceInfo proves
    that an appliance is answering and nothing more: one whose engine has never spoken to its IOCs
    answers it exactly like a healthy one, so the doctor printed ok for a deployment that was
    archiving nothing. That is a wiring fault, the class this tool exists to catch. The identified
    appliance is now also asked getApplianceMetrics, and two things make it a finding: channels held
    with none connected, or the appliance's own status reporting a stopped webapp. The second is
    the worse fault and is invisible in the counts, because when the engine webapp does not reply,
    mgmt cannot merge its numbers and they vanish from the row while pvCount survives, with the
    payload still served as HTTP 200.

  • New plane status no_ingest (~), exit 0. Deliberately not a failure: a freshly
    commissioned or fully paused appliance is legitimately in this state, and failing the run would
    make epics-doctor cry wolf in every CI job that calls it. It is not silent either, and that is
    what the next entry is for.

  • New --json field degraded_planes. The planes that proved their identity and are measurably
    not doing their job. It exists because every other signal stays clean for such a plane: ok and
    verification_complete remain true, unverified_planes and inconclusive_identity_planes
    remain empty, and identified_planes even lists it, since its identity IS proven. A script
    written against the documented field list would otherwise read a non-archiving archiver as
    positively confirmed. Purely additive; no existing field changes meaning and no exit code moves.

Changed

  • The archiver plane now issues three requests on a healthy run rather than two, and the ingest
    probe uses a single attempt with a 15 s floor instead of the shared retrying session. urllib3
    applies the timeout per attempt, and this route fans out to three internal requests per cluster
    member: measured against a 16-member cluster it answers in 7.3 s but takes 23.3 s to fail under
    the default 3-retry policy, which would have made the check blind there while slowing every run.
  • The archiver identity probe goes through the same shared beacon fetcher as every other plane. It
    was the one plane building its request inline, which already made that helper's "the one place
    every identity probe issues its request" contract untrue.
  • epics-doctor's config_error line now names the variable to set at its START, and the
    remedy points at that position.
    Measured against the last published release: 0.3.0 ended the
    observation with "Set EPICS_MCP_ARCHIVER_URL (the MGMT webapp URL)", the remedy table then took
    that instruction over, and it now reads "Set the variable named at the start of this finding",
    the construction unreachable already used. This matters beyond wording because detail is the
    field --json readers are told to use, so a consumer matching the older text will not find it.
    The position is guarded; the wording deliberately is not.
  • BREAKING: the import package is now epics_mcp (was epics_pv_mcp). import epics_pv_mcp
    stops working; import epics_mcp replaces it, one for one, with no other change to the API. This
    completes the rename begun in 0.3.0, where the distribution, the repository, the server command
    and the server's MCP identity already became epics-mcp and only the import package did not. It
    is done now rather than later because every release under the old import name grows the set of
    installations a rename breaks.
  • BREAKING: the epics-pv-mcp console command is removed. Use epics-mcp, which has been the
    primary command since 0.3.0. The alias was added in 0.3.0 for anyone following pre-rename docs,
    and 0.3.0 shipped it: every installation of that release carries epics-pv-mcp, so upgrading to
    0.4.0 removes a command that works today. Check your wrapper scripts and MCP client configs for
    it. Nothing older is affected, because 0.3.0 was this project's first published release of any
    kind. The four diagnostic commands (epics-doctor, epics-diagnose, epics-crossplane,
    epics-coverage) are unaffected.
  • BREAKING: the all extra is removed. pip install epics-mcp[all] no longer resolves; use
    epics-mcp[dev], which is what all contained. It was exactly epics-mcp[dev], so it promised a
    reader everything the package can do and delivered the developer toolchain, and the display-aware
    tools it seemed to imply are not an extra at all but a local dependency group. dev is now the
    only extra. Nothing else changes: no dependency is added or dropped by this.
  • The product is called EPICS MCP. The prose name was still "EPICS PV MCP Server" in 19 places,
    including the README H1 (which is the project page title on the index), the CITATION.cff title
    that published work cites, and the first line of the operator guide that ships in the wheel. The
    rename to epics-mcp in 0.3.0 was made because the PV plane is one of six, so a title saying PV
    contradicted its own reason. Nothing user-facing behaves differently; the distribution, the
    commands, the import package and the MCP identity are unchanged.
  • epics-doctor exits 1, not 2, on an internal error. 2 is the usage-error code across these
    commands and in argparse, so the old value told a wrapper the caller had passed something wrong
    when the command itself had failed. A genuine usage error (an unknown flag) still exits 2.
    Note the cost: exit 1 is also "a configured plane hard failed", so the exit code alone no longer
    separates the two. They differ on the streams: an internal error writes a doctor: line to
    stderr and no report to stdout.

Fixed

  • The display-aware CLIs tell a broken engine apart from a missing one. epics-crossplane and
    epics-coverage probed for the opi_navigation engine with a name lookup only, so an engine
    that was installed but did not import (typically a missing transitive dependency) passed the
    check and the command then died with a bare traceback. It now reports the failure with the
    underlying exception named and exits 1, while a genuinely absent engine keeps its explanation and
    exit 2. The advice differs too: installing the engine will not fix a broken one.
  • The core server no longer dies over an OPTIONAL capability. The display-capability probe called
    importlib.util.find_spec bare, and it runs at module level, so an import hook that RAISES for
    opi_navigation took the whole epics_mcp.server import down: exit 1 with a traceback, on a
    server whose PV tools never needed the display engine at all. The probe now answers instead of
    propagating. A module a finder reports as genuinely absent is the supported core-only state and
    stays silent, as before; a finder that could not answer is a different claim and is logged loud
    rather than wearing the "not installed" message. The sibling CLIs already behaved this way, so one
    package was answering the same question two ways.
  • A long live value in the find_device report is capped at 80 characters, not 82, as its own
    documentation promised.
  • The ready-to-paste write-enabled MCP client block now starts. As shipped it omitted
    EPICS_MCP_AUDIT_LOG_FILE and the loopback reach settings, both of which a write-enabled server
    refuses to start without, so a reader who pasted it saw only "server not connected".
  • The setup instructions no longer describe a .env file. Four places told adopters to copy
    .env.example to .env, or to run epics-doctor to confirm one. Nothing in the server ever
    loads a dotenv file; configuration is read from the process environment, and a variable that does
    not reach the process is dropped silently. .env.example is a reference to copy lines OUT of.
  • EPICS_MCP_ARCHIVER_RETRIEVAL_URL is spelled with its prefix in the README. The unprefixed
    form does not bind, so a split-appliance operator who followed it got history requests sent to
    the mgmt port with no error to explain it.
  • .env.example names all four tools the Olog write gate covers, not two. It was the only
    place that understated the gate's reach.
  • Every link on the PyPI project page now goes somewhere. The README is the long_description,
    and on that page there is no repository around it, so its relative targets ...
Read more

v0.3.0

Choose a tag to compare

@epicDirk epicDirk released this 29 Jul 19:27

Added

New tools

  • list_channel_vocabulary lists the ChannelFinder property keys and tag names that
    find_channels can be filtered on, as {enabled, properties, tags} (names only). properties
    is reduced to the same safe-property allowlist find_channels enforces, so it never advertises
    a key that would then be refused. An unreadable listing raises rather than reading as "there are
    none". Reuses EPICS_MCP_CHANNELFINDER_URL; no new variable.
  • get_appliance_info returns the Archiver Appliance's own topology (identity, the per-plane
    root URLs, cluster_inet_port, version), with no PV argument. It answers "am I pointed at the
    intended cluster before I trust list_archived_pvs or get_pv_history", where the wrong cluster
    silently yields a complete-looking list of the wrong PVs. A 404 means the wrong endpoint (the
    retrieval webapp serves /retrieval/bpl, not /mgmt/bpl) and propagates as an error.
  • list_log_levels returns the Olog level names plus default_level, or null and a note when
    the server does not state a default unambiguously. Call it before filtering a search by level: an
    unknown level returns 0 hits rather than an error.
  • update_log_entry edits an existing Olog entry's title, body, level, logbooks or tags.
    Whole-mode only. An omitted argument means unchanged: the tool round-trips the entry and
    overlays only what was passed, so attachments, properties and unedited fields survive the
    server's destructive full replace. The logbook allowlist is keyed on the union of current and
    resulting logbooks, because moving an entry into a logbook and pulling it out of one are both
    writes to that logbook. An entry whose attachments have duplicate or missing filenames is refused
    rather than edited, since attachment retention is filename-keyed.
  • add_log_attachment attaches files to an existing Olog entry. Whole-mode only, and purely
    additive for content. Note the server re-stamps owner to the write service account on every
    call, because this endpoint is its destructive update.
  • list_log_attachments and download_log_attachment list one entry's attachments and
    fetch raw bytes by (log id + filename) or GridFS id. Bytes bypass the entry redaction, so a
    download is withheld unless whole-mode and EPICS_MCP_OLOG_ALLOW_ATTACHMENT_DOWNLOAD are both
    in effect. An output_path must be a new file: it refuses to overwrite and refuses a symlink
    target.

New capabilities on existing tools

  • Attachments on write. create_log_entry and reply_to_log take attachments (workspace file
    paths, sent as multipart) and embed_image_base64 for a small inline image. Uploads ride the
    existing Olog write gate plus a size cap (EPICS_MCP_OLOG_ATTACH_MAX_BYTES), which also bounds
    the download body.
  • ChannelFinder query filters on find_channels: has_properties, lacks_properties,
    not_property_values, has_tags and lacks_tags, plus count_only for an exact, window-free
    match count from the /count endpoint. An unknown property name is a filter, not a silently
    ignored parameter, so a typo narrows the result to roughly zero. Property filtering is gated to
    the safe-property allowlist: filtering on a redacted property is refused, because it would
    reconstruct the partition the response projection hides.
  • level and title filters on search_logbook. title matches whole words, not substrings,
    and is a separate axis from text, which searches the body. A blank filter is refused before any
    request, because the server's answers to a blank value are misleading and disagree between the two
    fields. An empty level-filtered result carries a note when the value is not a configured level.
  • Archiver fields that were already fetched and then discarded are now surfaced, with no extra
    HTTP request. is_archived adds the getPVStatus connection-history cluster
    (connection_loss_regain_count, connection_first_established, connection_last_restablished).
    get_archive_info adds the alarm, display and control limits plus units, precision,
    controlling_pv, policy_name and modification_time. The nine numeric limits are always
    present and read "0.0" when the PV had no control info, so "0.0" does not necessarily mean a
    literal zero limit.
  • Write-side level validation. create_log_entry, reply_to_log and update_log_entry refuse
    a level the server does not list, and refuse a blank one separately, since a blank level would
    silently clear the entry's level. The check runs before the rate token, so a typo costs no token,
    and only when a level was passed.
  • entry_id in the FAILED write audit. The server archives and mutates before answering, so a
    timeout can leave an applied write in front of a client that sees FAILED. The record now names the
    entry.
  • Client-side consent hint on set_pv_value. Its tools/list entry carries
    _meta["anthropic/requiresUserInteraction"]=true. A client that honours it prompts a human on
    every call and fails closed when it cannot ask. This is advisory defense-in-depth only: the
    server-side write gate (env gate, regex allowlist, rate limit, audit) is unchanged and remains the
    sole client-independent guard.

Diagnostics

  • epics-doctor checks the alarm logger's Elasticsearch backend. The transport probe is a blind
    HEAD and reported the plane healthy even when the backend was dead. The identity probe now reads
    elastic.status from the same response body it already fetches and reports the new backend_down
    status (exit 1) when the logger says its backend is not Connected. This is distinct from
    unverified: identity is proven, and the service reports its own backend broken.
  • epics-doctor reports a failed identity probe as identity_probe_failed (glyph !, exit 3,
    INCONCLUSIVE) instead of collapsing it into unverified and exit 0. A 2xx that merely could not
    be named stays unverified and exit 0. New --json field inconclusive_identity_planes.
    Migration: a script that gated on exit 0 now sees exit 3 for a reachable but
    unidentifiable plane, and should read inconclusive_identity_planes alongside unverified_planes.
  • python -m epics_pv_mcp.find_moderate_pv, a read-only fixture finder that walks the Archiver
    MGMT event-rate report, filters a rate band, and counter-verifies each examined candidate against
    the target window with a real history fetch.

Changed

  • Renamed: the distribution is epics-mcp, the repository is epicDirk/EPICS-MCP (before
    anything was ever published, so nothing breaks for anyone). The old name undersold the server:
    the PV plane is one of six. The server command is now epics-mcp (the old epics-pv-mcp
    stays as an alias), and the server identifies itself to MCP clients and to the Olog as
    epics-mcp. The import package (epics_pv_mcp) and the four epics-* diagnostic commands
    keep their names for now; renaming those is a separate, deliberate step.
  • BREAKING: tool argument names unified. The tools carried four different argument names for
    "the PV this tool is about". They are now pv_name for a single PV (monitor_pv from name;
    is_archived, get_pv_history, get_archive_info, is_alarm_configured and get_alarm_history
    from pv) and pv_names for a list (get_pvs from names, validate_pvs from pvs).
    Unchanged: lookup_device_name.name (a device name, not a PV), the glob parameters pattern,
    name_pattern and query, and every output field. This affects INPUTS only; the output field
    of the five renamed REST tools is still pv, and aligning those would be a second breaking change
    for anyone reading results. MCP clients that call these tools by argument name must follow;
    positional calls are unaffected. The old name now returns a clean ToolError.
  • BREAKING: the server runtime moved from the SDK-bundled FastMCP 1.0 (mcp.server.fastmcp) to
    standalone fastmcp
    (fastmcp>=3,<4; mcp>=1,<2 stays for mcp.types.ToolAnnotations). This
    puts both project MCP servers on one stack and removes the two-ToolError-class hazard by
    construction, since only one fastmcp is on the path. Anyone embedding this server and catching
    mcp.server.fastmcp.exceptions.ToolError must switch to fastmcp.exceptions.ToolError.
  • Typed output schemas. 22 of the 32 tools now advertise a typed outputSchema instead of an
    open dict[str, object], so a caller can tell "no such PV" from "the service is not configured
    and I could not look", and a complete result from a truncated one. Notable field contracts:
    • find_channels returns disjoint fields per mode, and the configuration splits them again:
      a configured list gives {enabled, channels, total, capped}, a configured count gives
      {enabled, match_count}; unconfigured they are {enabled, channels, total, note} and
      {enabled, match_count, note}. enabled is the only field present on every path.
    • discover_pvs returns DiscoverPvsResult: pattern, pvs and total on every path, plus
      capped and source on the wildcard-with-ChannelFinder path and note on both wildcard paths.
    • list_archived_pvs, get_appliance_info, get_archive_info, list_channel_vocabulary and
      get_alarm_history carry typed schemas as well.
      The remaining untyped tools are the PV-value tools and the display-lane tools.
  • Three pre-gate refusal codes changed on the wire. A refusal raised before a write gate is
    consulted writes no audit line, so it must not wear the gate's error code: otherwise an
    un-audited refusal is indistinguishable from an audited gate DENY. OLOG_WRITE_DENIED became
    OLOG_WHOLE_MODE_REQUIRED for ...
Read more

v0.2.0

Choose a tag to compare

@epicDirk epicDirk released this 04 Jul 04:42

Read-only MCP server for EPICS control systems. Pre-1.0 (active development — the tool/API surface may still change).

What it is

An MCP server that lets an assistant read and diagnose an EPICS control system, read-only by default and localhost-isolated. It sees several planes: live PVs (p4p — PVAccess + Channel Access), the ChannelFinder registry, the Archiver Appliance, the Phoebus Alarm config, the ESS Naming Service, .bob displays and IOC st.cmd/.db.

Highlights

  • Core PV tools — read, batch-read, monitor, info, discover, validate connectivity (read-only; the single write tool is triple-gated and off by default).
  • Cross-plane — provenance (epics-crossplane), coverage audit (epics-coverage), and connection diagnosis (epics-diagnose) as both MCP tools and standalone CLIs.
  • Safety posture — no network egress until you widen the EPICS address list; REST planes stay disabled until their *_URL is set; opt-in path boundary.

This release

A large internal quality remediation landed for 0.2.0: the CLI⇄tool orchestration was unified, the four REST clients now share one retrying-session/GET substrate, the service layer no longer imports the tool layer (enforced by a guard test), the concurrent batch fallback was hardened, and coverage rose to ~93% (357 tests, mypy --strict, ruff clean).

See README.md, ARCHITECTURE.md, and CHANGELOG.md.