Skip to content

v0.3.0

Choose a tag to compare

@epicDirk epicDirk released this 29 Jul 19:27
· 494 commits to main since this release

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 the whole-mode preconditions of add_log_attachment and
    update_log_entry; RATE_LIMIT_EXCEEDED became READ_RATE_LIMIT_EXCEEDED for the opt-in
    read throttle; OLOG_ATTACH_TOO_LARGE became OLOG_ATTACH_TOO_LARGE_AT_READ for the
    post-admission size re-check. Every class stayed a subclass of the one it replaced, so except
    clauses are unaffected; a caller matching on the code string sees a different value. None of the
    three was ever released.
  • [INTERNAL] errors no longer carry the exception's raw text. An unexpected non-EpicsError
    at the tool boundary could put an internal detail (a request URL, a live PV name, a path) in front
    of the client. The client now receives [INTERNAL] <ClassName>; the full message and traceback are
    logged server-side at ERROR. The curated [<code>] message path is unchanged.
  • Olog refusals no longer report as INTERNAL. OlogError carries error_code as a class
    attribute and each subclass sets its own. INTERNAL read as transient and invited retries that
    each burned a rate token and wrote a FAILED line for a write that never happened.
  • epics-doctor no longer fails a service that answers with a different known service's name.
    It is reported unverified (exit 0) with the found name in the detail. A path-based reverse
    proxy can serve the real API behind a base URL that names another service, so the previous hard
    failure flagged working configurations.
  • EPICS_MCP_DEFAULT_TIMEOUT is honoured on the whole read and write path. Tool timeouts default
    to the configured server timeout instead of a hardcoded 5.0.
  • The live probe in diagnose_connection runs concurrently with the explanatory planes,
    so worst-case latency is about one timeout instead of two.
  • ChannelFinder property filter semantics are live-verified. The "unverified until a differential
    live probe" caveat is dropped for has_properties, lacks_properties, not_property_values and
    count_only. The tag filters (has_tags, lacks_tags) remain unverified. The "0 results does not
    distinguish an unknown property from an empty match" note stays, since it is structural.

Fixed

  • monitor_pv reported truncated on a complete, exactly-full stream. The service capped
    collection before appending, so it could not tell "the cap cut the stream" from "exactly
    max_events arrived, then it went quiet". It now over-collects one canary event, trims it, and
    reports truncated from the real comparison.
  • --timeout accepted -1, 0 and inf, misdiagnosing a healthy service as unreachable.
    epics-doctor, epics-diagnose and find_moderate_pv took a bare type=float, so a
    non-positive or non-finite timeout flowed into a live probe. A shared argparse type now rejects
    those as a usage error (exit 2) at parse time, before any probe.
  • Two shipped surfaces still taught the pre-rename argument names. The compare_machine_state
    prompt instructed get_pvs(names=[...]), to which the server answers "Missing required argument
    pv_names", and the epics-pv://guide resource still named pv for get_alarm_history. Both
    corrected, and a contract test now pins every documented tool(keyword=...) example against the
    live schema.
  • The attach documentation said the opposite of the truth. Five places claimed an attach
    preserves every field. The endpoint delegates to the server's updateLog, which sets the owner
    unconditionally, so attaching a file rewrites the entry's author. The claim is now narrowed to
    every content field.
  • The safety prose named 2 of the 4 write tools. EPICS_MCP_ALLOW_OLOG_WRITE gates
    create_log_entry, reply_to_log, add_log_attachment and update_log_entry, but README,
    ARCHITECTURE and the operator guide described it as the first two. A site admin approving the gate
    on that basis would unknowingly enable the destructive update_log_entry.
  • The archive was cited as a safety net it cannot be. Three places pointed at the server-side
    archived version to soften the owner re-stamp; no tool here can read or restore it. All three now
    say recovery is manual.
  • The level parameter descriptions disagreed across the tools that take one. All three now
    point at list_log_levels.
  • set_pv_value audit blind spot on cancellation. A write cancelled mid-put left no PV_WRITE
    record, even though the p4p put keeps running and may still land at the IOC. It now emits
    ATTEMPT with a correlation id before the I/O and UNKNOWN_PENDING on cancel, then re-raises the
    cancellation unchanged. It is never mislabelled FAILED and never blindly retried.
  • NamingServiceClient normalises base_url (trailing-slash strip) like the other REST clients,
    so a URL configured without a trailing slash no longer 404s.

Removed

  • The [displays] extra. pip install epics-pv-mcp[displays] no longer resolves. The extra
    pointed at the opi_navigation PV engine, which lives in a private repository, so no outside
    user could ever install it: the extra advertised a capability it could not deliver. The engine is
    now wired in as a local dependency group (uv sync --group displays), which keeps it working in
    a checkout that has access while keeping it out of the published package. The four display-aware
    tools (validate_pvs, crossplane_check, coverage_audit, find_device) are unaffected where
    the engine is present, and register themselves only when it is, exactly as before.
  • The post-registration schema-pruning pass (_prune_tool_schemas and three helpers, roughly 130
    lines) is gone. Standalone fastmcp emits the lean schemas natively, so the pass had nothing left
    to do. The one remaining need, dropping an accept-all outputSchema, is now the public
    output_schema=None on the tools that return an untyped dict.