--- name: asfw-mcp-control-plane description: Inspect and safely operate the local ASFW FireWire MCP control plane. Use when an ASFW task needs live driver status, node discovery, protocol telemetry, MCP tool discovery, or explicitly authorized guarded FireWire control through the loopback MCP endpoint. --- # ASFW MCP Control Plane Use the bundled client instead of reconstructing MCP HTTP/SSE sessions or pasting large tool schemas into the conversation. ## Default workflow Run these commands from the ASFireWire repository root. 1. Confirm the app-hosted MCP server is enabled. The default local endpoint is `http://127.0.0.1:8766/mcp`, which is also the client's built-in `DEFAULT_ENDPOINT`, so no export is needed in the normal case. The port is user-configurable in the app's **MCP Control Plane** settings. On `Connection refused`, read the port shown in that panel and set the endpoint explicitly rather than assuming the server is down: ```bash export ASFW_MCP_ENDPOINT=http://127.0.0.1:/mcp ``` 2. Ask the versioned health resource whether deeper reads are trustworthy: ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py health ``` `ready` permits targeted read-only diagnostics. `degraded` permits only high-level inspection until its reasons are resolved. `unavailable` means do not infer driver state. Preserve `expectedGeneration` for any follow-up bus request. 3. Run the compact read-only summary when node or protocol detail is needed: ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py summary ``` 4. Use `tools` or `resources` only when the summary lacks the needed detail. 5. Call a read tool with typed JSON arguments only after checking its schema: ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py tools python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_read_quadlet '{"nodeId":0,"generation":9,"addressHigh":65535,"addressLow":4026532864}' ``` ## Running driver build Before comparing live behavior with the checkout, read the commit embedded in the **running dext**. This uses the driver user client's version method and issues no FireWire transaction, so it is safe during active audio: ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_get_driver_version '{}' ``` Use `gitCommitFull`, `gitBranch`, `gitDirty`, and `buildTimestamp` from the response. The checkout's `DriverVersion.hpp` describes a build artifact and does not prove which dext is currently running. Pass `--endpoint` or set `ASFW_MCP_ENDPOINT` when the server uses a non-default loopback port. ## M-Audio 1814 Virtual UART diagnostics The 1814 shell can report its own isochronous counters, sync state, routing, and peak meters. Use the current physical `nodeId` and `expectedGeneration` from `summary`. These tools send FireWire mailbox writes even though the shell commands only read diagnostic state. They are gated as `developerWrite`, so do not call them without authorization for live device control. ```bash python3 tools/1814/bebob_shell.py --node --gen \ 'sys stat' 'sys avstat all' 'fw show' 'fw mix show' 'fw vol peak' ``` The MCP tool `asfw_bebob_shell_execute` accepts any single printable ASCII shell command and checks the 1814 identity and bus generation before each transaction. Commands may change device settings, so treat each call as a developer write. The five convenience tools `asfw_bebob_get_streaming_stats`, `asfw_bebob_get_silicon_status`, `asfw_bebob_get_sync_state`, `asfw_bebob_get_mixer_routing`, and `asfw_bebob_get_meter_peaks` use the same guarded path. `fw mix show` is routing readback; it does not change routing. ## Config-ROM explorer `asfw_get_config_rom` is a **read-only projection of the driver's discovery cache**. It never starts a ROM fetch and never issues a FireWire transaction. First obtain the current `nodeId` and generation from `summary`; after any bus reset, refresh them before asking for another view. The default `summary` view is deliberately compact: cache/generation status, GUID, vendor/model/unit identity, parser diagnostics, and two reminders that avoid common false conclusions. Use it before requesting the more detailed views: ```bash # Compact normal entry point: summary is default. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py rom 0 17 # BIB bitfields, each with bit position, decoded value, and a short meaning. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py rom 0 17 --view bib # Parsed IEEE 1212 directory tree (default 64 entries; raw leaf bytes omitted). python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py rom 0 17 --view tree # Big-endian cached quadlets; bounded to 64 per response and page by index. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py rom 0 17 --view raw --start-quadlet 0 --max-quadlets 32 ``` Equivalent direct calls are useful when another MCP client does not use the bundled script: ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_get_config_rom \ '{"nodeId":0,"generation":17,"view":"bib"}' ``` Interpret the annotated BIB carefully: - `IRMC`, `BMC`, `CMC`, and `ISC` are the device's Config-ROM capability claims. They do not establish physical root, designated IRM, or Bus Manager ownership. - `generation` in the BIB is the device's four-bit ROM field; it can differ from the current host topology generation supplied to the tool. - `max_rec` describes an asynchronous payload code. It is not an isochronous audio packet size. - A tree leaf reported as `not fetched from partial cache` is not evidence of a malformed ROM. The discovery cache may contain only a prefix. Likewise, a cached result from another generation is useful only as stale description, never as a target for a follow-up transaction. ## DICE registers by name `asfw_dice_read_register` accepts a **symbolic** `register` instead of a raw address. It resolves against the device's own section table, so no section base is ever assumed: ```bash # How many PCM channels does the device's playback (host->device) stream carry? python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_dice_read_register \ '{"nodeId":0,"generation":17,"register":"RX_NUMBER_AUDIO","streamIndex":0}' # Clock source and rate, decoded. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_dice_read_register \ '{"nodeId":0,"generation":17,"register":"GLOBAL_CLOCK_SELECT","decode":true}' ``` The response carries `resolvedAddress`, `sectionOffsetBytes` and (for per-stream registers) `streamConfigSizeBytes`, so the derivation is auditable rather than trusted. Omitting `register` falls back to the raw-address behaviour unchanged. **TX and RX are the device's directions, not the host's.** DICE `TX` is what the device transmits — the host's *capture*. DICE `RX` is what the device receives — the host's *playback*. ASFW's own profile fields use the opposite convention (`TxChannelCount` is playback), so never transcribe one onto the other. The offsets differ too: `TX_NUMBER_AUDIO` is at `0x0C` while `RX_NUMBER_AUDIO` is at `0x10`, because RX inserts `SEQ_START` ahead of it. Registers with no defined decoding return the raw value only; `decode: true` never invents an interpretation. `asfw_dice_decode_status` decodes a value you already hold, without touching the bus: ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_dice_decode_status \ '{"register":"GLOBAL_EXTENDED_STATUS","value":64}' ``` `GLOBAL_EXTENDED_STATUS` splits into `locked` and `slipping`; `arx1..arx4` are the device's *receive* streams, so `arx1` reflects the host-to-device transmit. These reads are `readOnly` but **not idempotent** — each issues a real FireWire transaction against a live generation. Do not run them during an active audio endurance run; prefer `asfw_get_audio_stream_health`, which reads driver-held counters and touches no bus. ## Tool visibility is not device capability `tools` lists a filtered view, never the full catalog. Two independent filters apply (`ASFW/MCP/ASFWMCPCore.swift`, `listTools`): 1. **Visibility tier vs runtime mode** — `always`, `readOnly`, `developerWrite`, and `rawDeveloper` are admitted according to the server's current mode. 2. **Protocol-hint prefilter** — evaluated *before* the tier check, against the union of `protocolHints` across all currently discovered nodes. The hint prefilter is **any-of, not all-of**: a tool is listed when it declares no hints, or when *at least one* of its declared hints is present. A tool declaring `["bebob", "cmp"]` is therefore listed on a `cmp`-only bus. Two consequences: - An absent tool means "not listed for this generation's hints and mode". It is **not** evidence that the driver lacks the capability or that the device lacks the protocol. Check `ASFWMCPToolCatalog` for the defined set and ask `asfw_explain_capability` for the specific reason. - Visibility does not follow the read-only/destructive gradient. Observed with only an Apogee Duet attached (`avc`, `cmp`): `asfw_phase88_start_48k` and `asfw_phase88_stop` are listed because they declare `cmp`, while every read-only BeBoB diagnostic (`asfw_bebob_get_unit_plug_info`, `asfw_bebob_get_clock_topology`, `asfw_phase88_get_clock`, …) is hidden because it declares `bebob` alone. The destructive lifecycle tools are reachable while the state queries that would justify using them are not. Never infer device state from which tools appear; re-list after any generation change. ## Ring-first diagnostic workflow The driver-owned log ring is the primary diagnostic source. Do not begin with `log stream` or add `ASFW_LOG` calls to interrupt/isochronous hot paths merely to investigate an incident. Query a small, relevant slice and preserve the returned `nextSequence` cursor for follow-up requests. 1. Run `health`, then record `expectedGeneration` from `summary`. 2. Read `asfw_log_stats` once to establish capacity, oldest/latest sequence, and drop count. A non-zero drop count makes an absence-of-evidence result weaker; report it. 3. Query one subsystem/category at a time with `maxRecords` no larger than 200 and, where possible, `contains`. Use `nextSequence` as the exclusive cursor on the next query. An empty sparse page may still advance the cursor. 4. Prefer a structured MCP read for current state and the ring for chronology. If an advertised read-only tool returns `capabilityUnavailable`/ `notImplemented`, report that adapter gap; do not fabricate the state or fall back to a mutation. Useful incident queries: ```bash # Was a reset requested locally, and which node's accepted Self-ID attributed it? python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query '{"categories":["BusReset"],"contains":"Reset ","maxRecords":100}' # Compare the physical topology role with Config-ROM capability claims. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query '{"categories":["ConfigROM"],"contains":"[RoleEvidence]","maxRecords":50}' # Inspect only CMP/FCP activity while audio is stopped or running. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query '{"categories":["CMP","FCP"],"maxLevel":"debug","maxRecords":200}' ``` ### Active audio-run rule During an active audio endurance or fault-reproduction run, do **not** call `health`, `summary`, discovery, or any control tool unless the user explicitly requests it. They can perturb the app/control plane while the issue is being timed. A small, targeted driver-ring query is permitted only when requested; it is read-only and never changes stream state. Do not run broad or parallel queries while audio is playing. For a live TX-content incident, query the retained `DirectAudio` lines. The `[TxPrep]` heartbeat carries the fill's health (`missedFinality`: frames that reached their packet too late and went out silent; `sOutMinPk`: the smallest margin, in packets, between a filled packet and the first packet the fill may still write, which is the projected hardware position plus 2). `[TxPrepRange]` is the refill-hole evidence (a producer pass that stopped below the descriptor floor): ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query \ '{"categories":["DirectAudio"],"contains":"[TxPrep","maxLevel":"debug","maxRecords":20}' ``` `[TxPrepFrame]` and `[TxExposure]` no longer exist (milestone 6, `documentation/TX_OWNERSHIP.md`); an older log that has them came from a driver before the audio-side fill. These lines are ring-only (`ASFW_LOG_RING_ONLY`), so read them through the driver log ring as above rather than `log show`. A healthy run shows only the periodic `[TxPrep]` heartbeat. The former `[PayloadWriter]` record was retired in FW-171: it was recorded from the real-time IO callback into the shared control block and drained on the receive path, and its query string (`[PayloadWriter] anomaly`) never matched an emitted line. ### Stream will not start: attribute it before theorising When a device enumerates but audio never flows, ask `asfw_get_audio_stream_health` **first**. It is a read-only projection of driver-held counters — no FireWire transaction, safe during an active run — and it separates three failures that otherwise present identically as silence. ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_get_audio_stream_health '{}' ``` Each endpoint carries a `verdict` and the raw `counters` behind it: | `verdict` | Means | |---|---| | `noPacketsReceived` | No packet reached the audio consumer. The IR context is not delivering — wrong iso channel, context never started, or the device stream was never enabled. Not a device-side fault yet. | | `geometryMismatch` | Packets arrived whose data block shape our stream config rejects. The **profile and the device disagree** on channels/DBS. Host-side rejection. | | `packetsRejected` | Packets arrived but failed decode (runt, undecodable CIP header, zero DBS). The device may be streaming correctly. | | `deviceSendsOnlyNoData` | Valid CIP headers with SYT `0xFFFF` and no audio frames. | | `dataNotAccepted` | Valid SYTs arrived but no replay entry was published. Inspect the SYT cadence detector, not the device. | | `receivingData` | Data is arriving and being accepted. | Read `deviceSendsOnlyNoData` precisely: it states **what the device sent**, not what the device is waiting for. It is not evidence that the device requires host timestamps first — the TCAT and Linux DICE stacks both withhold host SYT until the device has already sent valid ones, so a NO-DATA stall does not by itself imply a host-side handshake obligation. Report the counters; do not infer intent. `packetsSeen` counts every packet the master stream decoded, so `packetsSeen == noDataPackets` is the only sound basis for "the device is sending only NO-DATA". A non-zero reject counter with `packetsSeen > 0` means the device did send us something we threw away. ### Stream runs but sounds wrong: the stable telemetry summary Once a stream is up, `asfw_get_audio_telemetry` returns the full per-endpoint summary (wire v4, read-only, no transaction): TX preparation latency and committed-margin histograms, RX capture-ring occupancy/overrun/starvation, and for each completed interval **its duration and end time** — so counts can be turned into rates. `completedIntervalDurationNs: null` means the duration is unknown (first interval after a reset); do not divide by it. The `rxAttribution` member carries the same verdict as `asfw_get_audio_stream_health`. ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_get_audio_telemetry '{}' ``` This summary is deliberately small and stable. It is not a research trace: new experiments go to the log ring, never into this contract (`documentation/OBSERVABILITY_INVENTORY.md`). Pair it with the bring-up records, which are emitted for a stream that has **not** established yet (bounded per start, so a healthy stream stays silent): ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query \ '{"categories":["DirectAudio"],"contains":"[RxReplayReset] phase=bootstrap","maxLevel":"debug","maxRecords":20}' ``` ### Audio timing-loss first-fault query For an AV/C duplex click, stall, or timing-loss recovery, query the driver ring before theorizing about SYT phase. The driver emits `[RxReplayReset]` exactly when an already-established RX replay epoch is invalidated and before it calls the recovery callback. It is anomaly-only; a healthy stream has no matching records. ```bash python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_stats '{}' python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query \ '{"afterSequence":0,"categories":["DirectAudio"],"contains":"[RxReplayReset]","maxLevel":"debug","maxRecords":20}' ``` The record's `reason` identifies the layer that failed: - `packet-status`: CIP/payload decoding or input-buffer writing rejected a packet. - `invalid-rx-timestamp`: descriptor timestamp could not be correlated with the drain cycle. - `receive-cycle-gap`: ASFW observed a gap in its one-received-packet-per-cycle model. - `syt-cadence-rejected`: a valid SYT produced an invalid cadence delta. - `clock-anchor-rejected`: RX could not publish a host clock anchor. Always report `droppedRecords` from `asfw_log_stats`. A zero drop count makes the first matching record authoritative for the local reset; it does not by itself prove whether the original fault was device-side or host-side. After the reset-provenance change is installed, interpret the two records as a pair: `Reset request: origin=local ...` describes ASFW's outgoing action; `Reset provenance: ... initiator=nodeN` is the accepted Self-ID attribution for the resulting bus generation. Never attribute a remote reset to ASFW just because it followed a local request. ## FireWire role evidence Keep these four facts separate in reports and code reviews: - **Physical root** comes from the Self-ID topology tree. - **Designated IRM** is the highest physical-ID Self-ID node with contender and link-active asserted. It is an operational designation, not proof that its resource CSRs have been successfully used. - **BIB capabilities** (`IRMC`, `BMC`, `CMC`, `ISC`) are Config-ROM claims. - **Bus Manager ownership** comes from `BUS_MANAGER_ID` election/readback, not from root or IRM status. Legacy devices can disagree across these sources. The observed Apogee Duet example is node 2: its BIB q2 is `0x20FF5003` (`IRMC=0`, `BMC=0`, `CMC=0`, `ISC=1`), while its accepted Self-ID is link-active + contender and therefore designates it as the IRM/root. It is **not** a Bus Manager. Preserve both facts; do not "correct" topology from BIB flags or infer that it is fighting for BM. For isochronous allocation, verify the designated IRM by the normal guarded resource transaction at allocation time rather than rejecting it from BIB alone. ## Config-ROM cache rule The discovery cache can contain only a fetched prefix of a Config ROM. A leaf target outside that cached prefix is not, by itself, a malformed-device-ROM finding. Describe it as `not fetched from partial cache` unless an explicit, generation-pinned full read proves the target is outside the actual ROM. Same-generation retry data must never replace a richer node cache with a shorter prefix. If the UI reports a same-generation size regression, query `ConfigROM` ring records and treat it as a cache lifecycle bug, not proof that the device changed its ROM. ## Safety - Treat `summary`, `tools`, `resources`, and `read` as the normal path. - The client refuses non-allowlisted tool calls unless `--allow-mutation` is present. This flag is only a local acknowledgement; the MCP server's developer/mutation policy remains authoritative. - Use `--allow-mutation` only after the user explicitly authorizes the exact hardware action. Do not infer permission from a request to inspect, diagnose, capture, or test. - Read and preserve the current generation. A generation change means prior node IDs and writes are stale; refresh the summary before any follow-up action. - Do not use the skill to issue raw writes or control commands merely to probe a device. Prefer the MCP hardware-smoke runner's read-only mode. - `asfw_apogee_duet_apply_format_dev` is an intentional interruption: use it only when the user authorizes the exact rate change, includes `acknowledgeInterruption: true`, and has confirmed that audio is stopped. It is not suitable for discovery or routine diagnostics. - Do not rebuild, install, reload, or reset hardware while diagnosing unless the user explicitly asks. A user-built/install driver is the live artifact; source changes do not affect it until the user elects to rebuild. ## Focused commands ```bash # Discover tool/resource names without expanding every response in the prompt. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py tools python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py resources # Read an advertised resource. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py read asfw://telemetry/snapshot # Inspect a read-only tool's full result when needed. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_avc_list_units '{}' # Query the bounded driver-owned log ring. `nextSequence` is an exclusive # cursor for the next call; an empty page can still advance it when a sparse # filter consumed its scan budget. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_query '{"categories":["CMP"],"contains":"iPCR","maxLevel":"debug","maxRecords":200}' python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_log_stats '{}' # BridgeCo/BeBoB generic unit PLUG_INFO (fixed, STATUS-only FCP command). python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_bebob_get_unit_plug_info '{"targetGuid":3003878663639543,"nodeId":0,"generation":2}' # Music Subunit SYNC input and current BridgeCo clock-source topology. python3 skills/asfw-mcp-control-plane/scripts/asfw_mcp.py call asfw_bebob_get_clock_topology '{"targetGuid":3003878663639543,"nodeId":0,"generation":2}' ``` If the endpoint is unavailable, report the connection failure and ask the user to enable the MCP Control Plane in ASFW. Do not fall back to guessed driver state. ## Optional Console correlation Do not use Console as the normal ASFW driver-diagnosis path: the MCP driver ring is retained, queryable, and gives the chronology needed for transport/audio incidents. Unified-log correlation is optional only when the question lies outside the ring, such as app/UI behaviour around an MCP call. ```bash log stream --info --debug --predicate 'eventMessage CONTAINS "[MCP]" OR eventMessage CONTAINS "[UserClient]" OR eventMessage CONTAINS "[FCP]"' ``` ## PHASE 88 / BeBoB discovery rule For the exact TerraTec PHASE 88 Rack FW identity (vendor `0x000AAC`, model `0x000003`), do **not** gate BeBoB discovery behind generic AV/C `UNIT_INFO` or `SUBUNIT_INFO`. Linux BeBoB begins with generic unit `PLUG_INFO`, then BridgeCo extended ISO-plug type and stream-format-list STATUS commands. - A generic AV/C inventory with zero subunits or zero ISO plugs is not evidence that the device has no BeBoB capabilities. - Preserve the BridgeCo operand layout: its unit address is the AV/C subunit byte (`0xff`); the extension operands begin with direction. Stream-format list places support-status before entry index. - Before treating a BeBoB STATUS command as unsupported, distinguish an FCP transport failure or bus reset from an AV/C `REJECTED`/`NOT IMPLEMENTED` response. Include `[FCP]` in the user-provided unified-log predicate.