diff --git a/docs/planning/mcp/GITHUB_ISSUES.md b/docs/planning/mcp/GITHUB_ISSUES.md index deb5ac1..bd67682 100644 --- a/docs/planning/mcp/GITHUB_ISSUES.md +++ b/docs/planning/mcp/GITHUB_ISSUES.md @@ -49,29 +49,36 @@ src/ #### Implementation phases -This epic is broken down into 12 implementation issues, designed to be worked roughly sequentially with some parallelism possible (see dependency graph at the bottom of this doc). +This epic is broken down into 14 implementation issues, designed to be worked roughly sequentially with some parallelism possible (see dependency graph at the bottom of this doc). | Issue | Title | Effort | Depends on | |---|---|---|---| | #mcp-1 | Project scaffold + CI | 4h | — | -| #mcp-2 | Discovery & connection tools | 6h | #mcp-1 | +| #mcp-2 | Discovery & connection tools (+ capabilities caching) | 8h | #mcp-1 | | #mcp-3 | Device introspection tools | 3h | #mcp-2 | -| #mcp-4 | Channel configuration tools | 5h | #mcp-2 | -| #mcp-5 | Streaming infrastructure (ring buffer, summarizer) | 8h | #mcp-1 | +| #mcp-4 | Channel configuration tools | 5h | #mcp-2, #mcp-13 | +| #mcp-5 | Streaming infrastructure + quiescence gate | 11h | #mcp-1, #mcp-2 | | #mcp-6 | Streaming tools | 6h | #mcp-4, #mcp-5 | -| #mcp-7 | SD card tools (non-destructive) | 5h | #mcp-2 | -| #mcp-8 | Resources (daqifi://) | 4h | #mcp-3, #mcp-6 | +| #mcp-7 | SD card tools (non-destructive) | 5h | #mcp-2, #mcp-13 | +| #mcp-8 | Resources (daqifi://) | 5h | #mcp-3, #mcp-6, #mcp-14 | | #mcp-9 | Safety model | 5h | #mcp-1 | | #mcp-10 | Starter recipe prompts | 6h | #mcp-6, #mcp-7 | | #mcp-11 | Distribution (AOT, dotnet tool, npx) | 8h | all tool issues | | #mcp-12 | Documentation rewrite | 6h | #mcp-10, #mcp-11 | +| #mcp-13 | Structured SCPI error contract (`ConfirmedScpiResult`) | 8h | #mcp-1 | +| #mcp-14 | Cross-cutting infra (audit log, rate limiter, logging) | 10h | #mcp-1, #mcp-2 | -**Total estimated effort:** ~66 hours (~4–6 calendar weeks part-time). +**Total estimated effort:** ~90 hours (~6–8 calendar weeks part-time). + +**Separate from this epic (follow-up cleanup PR):** `Daqifi.Core` API additions (`GetMetadataAsync`, `ExecuteConfirmedAsync`) — see [RFC §Cross-cutting concern for daqifi-core](RFC.md#cross-cutting-concern-for-daqifi-core). Not blocking; reduces MCP boilerplate and also benefits `daqifi-desktop`. Filed separately from the MCP epic. #### Success criteria - [ ] `Daqifi.Mcp` project builds and produces an AOT binary per supported OS - [ ] Claude Desktop (or any STDIO-MCP client) can launch the server, discover a Nyquist device, configure 4 analog channels, start streaming, and retrieve a stream summary +- [ ] All v0.1 tools return the structured `{success, response, errors[], warnings[]}` error shape (see #mcp-13) +- [ ] Quiescence gate is enforced: tools that would issue SCPI against a streaming device return a structured `quiescence_violation` error rather than corrupting the stream (see #mcp-5) +- [ ] Variant-aware capabilities: NQ1/NQ2/NQ3 differences are surfaced via `daqifi://devices/{id}/capabilities` and applied as sane defaults (see #mcp-2, #mcp-14) - [ ] All v0.1 tools have integration tests covering happy-path behavior - [ ] Safety modes (`read-only`, `control`, `admin`) gate destructive operations correctly - [ ] At least 5 recipe prompts ship and are demonstrable on real hardware @@ -168,7 +175,8 @@ Implement the four foundational tools that let an agent find devices and open co - [ ] Implement `Tools/DiscoveryTools.cs` with the four tools above - [ ] `discover_devices` wraps `WiFiDeviceFinder` + `SerialDeviceFinder`, merges results, mints stable `device_id` (suggest `{transport}:{serial_or_address}`) -- [ ] `connect_device` calls `ConnectFromDeviceInfoAsync()`, registers in `DeviceRegistry` +- [ ] `connect_device` calls `ConnectFromDeviceInfoAsync()`, reads `*IDN?` + `SYST:SYSInfoPB?` to populate a `BoardCapabilities` blob, registers handle in `DeviceRegistry` with capabilities attached +- [ ] Define `BoardCapabilities` record (`variant: NQ1|NQ2|NQ3`, `analog_channels`, `digital_channels`, `adc_bits`, `default_voltage_precision`, `has_dac`, etc.) — populated from firmware at connect time, not hard-coded per variant - [ ] `disconnect_device` removes from registry and disposes the device - [ ] `list_connected_devices` enumerates `DeviceRegistry` - [ ] All inputs validated; errors return MCP error responses with actionable messages @@ -178,19 +186,21 @@ Implement the four foundational tools that let an agent find devices and open co - [ ] Agent can call `discover_devices` and receive a list of devices - [ ] Agent can call `connect_device` with a `device_id` from discovery and the device becomes connected +- [ ] `BoardCapabilities` is populated correctly for NQ1 (12-bit, no DAC), NQ3 (18-bit, has DAC), NQ2 (24-bit) — variant detected from firmware response, not the caller - [ ] `list_connected_devices` reflects the connected state - [ ] `disconnect_device` cleans up cleanly; subsequent `list_connected_devices` reflects disconnection - [ ] Tool descriptions are written for agent ergonomics (clear, one-sentence purpose; parameter docs include units and examples) -- [ ] Integration test passes against real hardware +- [ ] Integration test passes against real hardware (skip-on-no-hardware when not available) ### Files to create - `src/Daqifi.Mcp/Tools/DiscoveryTools.cs` +- `src/Daqifi.Mcp/Server/BoardCapabilities.cs` - `src/Daqifi.Mcp.Tests/Tools/DiscoveryToolsTests.cs` ### Estimated effort -6 hours +8 hours ### Dependencies @@ -295,17 +305,17 @@ Implement tools that let an agent enumerate and configure analog/digital channel --- -## #mcp-5: Streaming infrastructure (ring buffer + summarizer) +## #mcp-5: Streaming infrastructure + quiescence gate ### Title -MCP: Streaming infrastructure — ring buffer, summarizer, window reader +MCP: Streaming infrastructure — ring buffer, summarizer, window reader, quiescence enforcement ### Labels -`mcp`, `infrastructure`, `streaming`, `phase-3` +`mcp`, `infrastructure`, `streaming`, `safety`, `phase-3` ### Objective -Build the server-side infrastructure that lets agents reason about live waveform data without pulling raw samples into context. **This is the differentiating technical capability** of the v0.1 release. +Build the server-side infrastructure that lets agents reason about live waveform data without pulling raw samples into context **and** enforce the firmware quiescence rule (no SCPI during a benchmarked stream — see [RFC §Quiescence rule](RFC.md#quiescence-rule--no-scpi-during-a-benchmarked-stream)). **This is the differentiating technical capability** of the v0.1 release, and the quiescence gate is the #1 footgun protection for an LLM driving the device. ### Components @@ -313,6 +323,8 @@ Build the server-side infrastructure that lets agents reason about live waveform - `Streaming/StreamSummarizer.cs` — computes per-channel `{n, min, max, mean, rms, std, dominant_freq_hz, sparkline}` over a configurable window. Sparkline is a 40-char Unicode block-character string. - `Streaming/WindowReader.cs` — given `(start_s, end_s, max_points)`, returns decimated samples (decimation factor chosen to stay under `max_points`). - `Server/StreamRegistry.cs` — tracks active streams by `stream_id`. +- `Safety/RequiresQuiescenceAttribute.cs` — marker attribute applied to tool methods that issue SCPI to the device. +- `Safety/QuiescenceGate.cs` — dispatcher hook that consults `DeviceRegistry.IsStreaming(deviceId)` and rejects gated tool calls with a structured `quiescence_violation` error. ### Tasks @@ -322,7 +334,10 @@ Build the server-side infrastructure that lets agents reason about live waveform - [ ] Implement `WindowReader` with server-side decimation (averaging buckets) - [ ] Implement `StreamRegistry` with stream lifecycle (start, stop, dispose) - [ ] Wire streaming subscription to `IDaqifiDevice` message events -- [ ] Unit tests for ring buffer correctness, summarizer math (against a known sine wave), and window decimation +- [ ] Extend `DeviceRegistry` (from #mcp-1) with `IsStreaming(deviceId): bool` — set by `start_streaming`, cleared by `stop_streaming` / disconnect / auto-stop +- [ ] Implement `RequiresQuiescenceAttribute` and the dispatcher hook that rejects calls with a structured error containing actionable guidance (e.g. *"Cannot run `get_device_status` while `` is streaming — would invalidate the measurement. Use `get_stream_summary` for in-process state, or call `stop_streaming` first."*) +- [ ] Support `respectQuiescence: false` opt-out parameter — logged in the audit trail (see #mcp-14) so opt-outs are visible +- [ ] Unit tests for ring buffer correctness, summarizer math (against a known sine wave), window decimation, and quiescence gate (mocked DeviceRegistry) - [ ] Benchmark: 1000 Hz × 16 channels × 60 s ring buffer must stay under 50 MB RAM ### Acceptance criteria @@ -330,6 +345,8 @@ Build the server-side infrastructure that lets agents reason about live waveform - [ ] Ring buffer correctly retains the last N seconds of samples per channel - [ ] Summarizer returns mathematically correct stats for a known input (sine wave: mean ≈ 0, rms ≈ amplitude/√2, dominant_freq matches input) - [ ] Window reader respects `max_points` cap and returns decimated data when input exceeds cap +- [ ] Quiescence gate rejects a `[RequiresQuiescence]` tool call against a streaming device with a structured error; allowlisted in-process tools (`stop_streaming`, `disconnect_device`, `get_stream_summary`, `read_stream_window`) pass through +- [ ] `respectQuiescence: false` opt-out works and is captured in audit log - [ ] All unit tests pass - [ ] Benchmark target met @@ -339,15 +356,19 @@ Build the server-side infrastructure that lets agents reason about live waveform - `src/Daqifi.Mcp/Streaming/StreamSummarizer.cs` - `src/Daqifi.Mcp/Streaming/WindowReader.cs` - `src/Daqifi.Mcp/Server/StreamRegistry.cs` +- `src/Daqifi.Mcp/Safety/RequiresQuiescenceAttribute.cs` +- `src/Daqifi.Mcp/Safety/QuiescenceGate.cs` - `src/Daqifi.Mcp.Tests/Streaming/*Tests.cs` +- `src/Daqifi.Mcp.Tests/Safety/QuiescenceGateTests.cs` ### Estimated effort -8 hours +11 hours ### Dependencies - #mcp-1 (Project scaffold) +- #mcp-2 (DeviceRegistry needs to track per-handle streaming state) --- @@ -706,31 +727,172 @@ Reposition the repository's README around the agent workflow, and ship a dedicat --- +## #mcp-13: Structured SCPI error response contract + +### Title +MCP: Implement structured SCPI error response contract (`ConfirmedScpiResult`) + +### Labels +`mcp`, `infrastructure`, `error-contract`, `phase-2` + +### Objective + +Implement the uniform `{success, response, errors[], warnings[]}` response shape that every SCPI-issuing tool returns in v0.1. Without this, the LLM has to grep free text to know whether a command failed and misses asynchronous errors entirely. See [RFC §Structured error response contract](RFC.md#structured-error-response-contract-all-scpi-tools--v01-work). + +### Components + +- `Server/ConfirmedScpiResult.cs` — the result record + error/warning types +- `Server/ConfirmedScpiExecutor.cs` — wraps `Daqifi.Core`'s text-channel SCPI execution with before/after error-queue drain, optional async log scrape, and readback validation + +### Tasks + +- [ ] Define `ConfirmedScpiResult` record: `{Success: bool, ResponseText: string, Errors: IReadOnlyList, Warnings: IReadOnlyList}` +- [ ] Define `ScpiError` with `Source: "SCPI" | "LOG_E" | "READBACK"`, `Code: int?`, `Message: string` +- [ ] Define `ScpiWarning` with `Code: string` (e.g. `UNDOCUMENTED_SCPI`), `Message: string`, optional `Suggestions: string[]` +- [ ] Implement `ConfirmedScpiExecutor.ExecuteAsync(command, drainErrorQueue=true, scrapeLogAfter=false, readback=null, ct)`: + - drain `SYST:ERR?` before execution to establish a clean baseline + - send the command + - drain `SYST:ERR?` after execution; the delta becomes the synchronous errors + - if `scrapeLogAfter=true`, drain `SYST:LOG?` and capture any `[ERROR]` / `[WARN]` lines (note: this drains the buffer) + - if a readback validator is provided, run it; on timeout/failure, append a `READBACK` error + - `Success = false` iff any synchronous SCPI error or readback failure occurred (async log lines are warnings unless explicitly marked errors) +- [ ] Defaults: typed tools opt in to `scrapeLogAfter=false` (avoid drain overhead per call); `send_scpi` (v0.2) defaults `scrapeLogAfter=true` +- [ ] Bundle a snapshot of `daqifi-nyquist-firmware`'s `01-SCPI-Interface.md` wiki under `src/Daqifi.Mcp/Resources/scpi-wiki-snapshot.md` (text file) with a refresh script `scripts/refresh-scpi-wiki.sh` +- [ ] CI step in `.github/workflows/ci.yml`: fail the build if `scpi-wiki-snapshot.md` is older than 30 days (use `git log -1 --format=%ct` on the file) +- [ ] Unit tests covering: clean run, sync SCPI error, async LOG_E line, readback timeout, all three at once +- [ ] Refactor existing typed-tool issue scaffolds (#mcp-4, #mcp-7) to return the structured shape — task list updated in those issues + +### Acceptance criteria + +- [ ] All SCPI-issuing tools return the documented response shape +- [ ] A failing command surfaces all three error classes in one response when applicable +- [ ] Async log scrape opt-in works and is destructive to the log buffer (documented behavior) +- [ ] CI fails when the wiki snapshot is >30 days old +- [ ] Snapshot date is exposed via a resource for runtime introspection (`daqifi://session/scpi-snapshot-info`) + +### Files to create + +- `src/Daqifi.Mcp/Server/ConfirmedScpiResult.cs` +- `src/Daqifi.Mcp/Server/ConfirmedScpiExecutor.cs` +- `src/Daqifi.Mcp/Resources/scpi-wiki-snapshot.md` (bundled snapshot) +- `scripts/refresh-scpi-wiki.sh` +- `src/Daqifi.Mcp.Tests/Server/ConfirmedScpiExecutorTests.cs` + +### Files to modify + +- `.github/workflows/ci.yml` (add freshness check) + +### Estimated effort + +8 hours + +### Dependencies + +- #mcp-1 (Project scaffold) + +### Notes + +- This issue exists in v0.1 specifically so the contract is established *before* tool implementations land. Tool issues #mcp-4, #mcp-6, #mcp-7 should be coded against `ConfirmedScpiResult` from day one. +- The wiki snapshot bundled here is the foundation the v0.2 `send_scpi` hallucination-warn feature will read from. Refresh script + CI check should be in place before the snapshot is needed for warning lookups. + +--- + +## #mcp-14: Cross-cutting infrastructure (audit, rate limit, logging) + +### Title +MCP: Implement audit log, rate limiter, and structured logging conventions + +### Labels +`mcp`, `infrastructure`, `safety`, `phase-3` + +### Objective + +Implement the three pieces of cross-cutting infrastructure that the MCP needs in v0.1 to be operable and debuggable: an audit log (every SCPI command + response, with credential redaction), a per-handle rate limiter (defends against runaway LLM loops), and disciplined stdout/stderr usage (stdout is JSON-RPC only). See [RFC §Cross-cutting infrastructure](RFC.md#cross-cutting-infrastructure-v01-internal). + +### Components + +- `Server/ScpiAuditLog.cs` — bounded session-scoped log of every SCPI command + response, with sensitive argument redaction +- `Server/RateLimiter.cs` — token-bucket per-handle rate limiter +- `Resources/SessionResources.cs` — exposes `daqifi://session/audit` and `daqifi://session/scpi-snapshot-info` +- `Server/LoggingSetup.cs` — `Microsoft.Extensions.Logging` to stderr; stdout reserved for JSON-RPC + +### Tasks + +- [ ] Implement `ScpiAuditLog` with bounded capacity (default 2000 entries, configurable via `--audit-log-size`) — ring-buffer eviction +- [ ] Each entry: `{timestamp, device_id, tool_name, scpi_command, response_summary, errors, warnings, opt_outs}` +- [ ] Redaction: any argument key matching `/(?i)pass|secret|key|token/` has its value replaced with `***REDACTED***` at log emission time (not at input time — the password still works for the SCPI call) +- [ ] Wire audit log into `ConfirmedScpiExecutor` (from #mcp-13) so every SCPI call is recorded +- [ ] Implement `RateLimiter` with per-handle token bucket: default 30 tokens/sec, configurable via `--max-tool-calls-per-second` +- [ ] Rate-limit exceeded returns a structured `rate_limit_exceeded` error with retry-after hint +- [ ] Expose `daqifi://session/audit` resource (read-only, paginated) +- [ ] Expose `daqifi://session/scpi-snapshot-info` resource (snapshot date, source URL, last-refresh attempt result) +- [ ] Configure `Microsoft.Extensions.Logging` to write to stderr with structured JSON output +- [ ] CI/test: assert that stdout contains only valid JSON-RPC framing — any errant `Console.WriteLine` causes test failure +- [ ] Unit tests for audit log eviction, redaction patterns, rate limiter math + +### Acceptance criteria + +- [ ] Audit log captures the last N SCPI calls with arguments, response, and errors/warnings +- [ ] Credentials are redacted in the audit log but still work in the actual SCPI call +- [ ] Rate limiter blocks at the configured threshold and returns a structured error +- [ ] No stray output corrupts the stdout JSON-RPC stream (test enforced) +- [ ] `daqifi://session/audit` resource is readable from an MCP client +- [ ] `quiescence_violation` opt-outs (from #mcp-5) appear in the audit log + +### Files to create + +- `src/Daqifi.Mcp/Server/ScpiAuditLog.cs` +- `src/Daqifi.Mcp/Server/RateLimiter.cs` +- `src/Daqifi.Mcp/Server/LoggingSetup.cs` +- `src/Daqifi.Mcp/Resources/SessionResources.cs` +- `src/Daqifi.Mcp.Tests/Server/ScpiAuditLogTests.cs` +- `src/Daqifi.Mcp.Tests/Server/RateLimiterTests.cs` + +### Estimated effort + +10 hours + +### Dependencies + +- #mcp-1 (Project scaffold) +- #mcp-2 (needs DeviceRegistry for per-handle state) + +--- + ## Dependency graph ``` - #mcp-1 (scaffold) - / | \ - #mcp-2 (discovery) #mcp-5 (stream infra) #mcp-9 (safety) - / | \ \ - #mcp-3 #mcp-4 #mcp-7 \ - (info) (channels) (SD) \ - \ \ \ - +--- #mcp-6 (streaming tools) - \ - +--- #mcp-8 (resources) - \ - +--- #mcp-10 (prompts) - \ - #mcp-11 (distribution) - \ - #mcp-12 (docs) + #mcp-1 (scaffold) + ┌──────────┬───┴──────┬─────────┬──────────┐ + │ │ │ │ │ + #mcp-2 #mcp-9 #mcp-13 #mcp-5 (needs #mcp-2 for IsStreaming) + (discovery+ (safety) (error + capabilities) contract) + │ │ + ┌────────┬───┴────┬────────┐ │ + │ │ │ │ │ + #mcp-3 #mcp-4 #mcp-7 #mcp-14 (#mcp-4/#mcp-7 also depend on #mcp-13) + (info) (channels) (SD) (audit+ + ratelimit) + │ │ + └───┬──── #mcp-6 ────┘ + │ (streaming tools) + ├──── #mcp-8 (resources) + └──── #mcp-10 (prompts) + │ + #mcp-11 (distribution) + │ + #mcp-12 (docs) ``` -Issues #mcp-1, #mcp-5, and #mcp-9 can start in parallel. -Issues #mcp-2/3/4/7 can be parallelized after #mcp-1. -Issues #mcp-6/8/10 unblock once their dependencies land. -Issues #mcp-11/12 finalize the release. +Parallelism notes: +- #mcp-1 unblocks everything else. +- #mcp-2, #mcp-9, #mcp-13 can run in parallel right after #mcp-1. +- #mcp-5 needs #mcp-2 (DeviceRegistry tracks `IsStreaming`). +- #mcp-14 needs #mcp-2 (per-handle audit) and integrates with #mcp-13's executor. +- #mcp-3/#mcp-4/#mcp-7 fan out after #mcp-2; #mcp-4/#mcp-7 also wait on #mcp-13 so they return the right error shape from day one. +- #mcp-6/#mcp-8/#mcp-10 unblock once their dependencies land. +- #mcp-11/#mcp-12 finalize the release. --- @@ -743,6 +905,7 @@ Issues #mcp-11/12 finalize the release. - `streaming` - `sd-card` - `safety` +- `error-contract` - `prompts` - `release` - `phase-1`, `phase-2`, `phase-3`, `phase-4` diff --git a/docs/planning/mcp/RFC.md b/docs/planning/mcp/RFC.md index 60215ab..20f6c09 100644 --- a/docs/planning/mcp/RFC.md +++ b/docs/planning/mcp/RFC.md @@ -138,7 +138,7 @@ Full reference is in the [appendix](#appendix-complete-tool-reference). Summary: | SD card (destructive) | `delete_sd_file`, `format_sd_card` | v0.2 | | Network | `configure_wifi`, `configure_static_ip` | v0.2 | | Firmware | `check_firmware_update`, `update_firmware` | v0.2 | -| Escape hatch | `send_scpi` | v0.2 (behind `--allow-raw-scpi`) | +| Escape hatch | `send_scpi` | v0.2 (design specced in [§Safety: send_scpi v0.2 design](#safety-send_scpi-v02-design)) | #### The streaming layer is the differentiator @@ -159,7 +159,11 @@ Build this surface and *copy it from yourself* when competitors catch up. ### Prompts (the "recipes") -These ship with v0.1 and **are the marketing**. Each one becomes a YouTube demo and a README snippet. +**What a "recipe" is, mechanically:** an MCP server exposes three surfaces — tools (callable functions), resources (read-only URIs), and **prompts** (parameterized templates the server tells the client about). The MCP-aware client (Claude Desktop, Cursor, Cline) surfaces those prompts as a slash-command menu / suggestion list / parameter form. When the user picks one and fills in the parameters, the client expands the template into the LLM's context as a structured user-or-system message — the LLM then has both a known-good starting prompt AND the agent's normal tool access to act on it. + +So a "recipe" in this RFC = an MCP prompt that bundles a domain-specific task description with sensible defaults the user can override. The LLM doesn't have to *know* how to set up a thermocouple sweep — the recipe tells it the right tool sequence and the user only fills in *"how many channels, what threshold, where to log."* + +These ship with v0.1 and **are the marketing** — they're the difference between "another MCP server" and "the agent already knows how to run my lab." Each one becomes a YouTube demo and a README snippet: - `setup_thermocouple_sweep` — multi-channel temperature, sample rate, SD logging, threshold alerting - `battery_soak_test` — long-duration logging with hourly stat summaries @@ -167,6 +171,8 @@ These ship with v0.1 and **are the marketing**. Each one becomes a YouTube demo - `multi_channel_pressure_test` — the medical-R&D pattern called out in the README - `wifi_provision_new_device` — out-of-box onboarding via agent +For the deferred "hosted recipe library" question (user-contributed recipes shared back to a public registry), see [Open Questions Q7](#q7-hosted-recipe-library). + ### Safety model Three server-launch modes plus a confirmation pattern: @@ -188,6 +194,82 @@ Plus: Bake this in for v0.1. It becomes a marketing point ("agent-safe by design") and it is much harder to retrofit after the first incident. +#### Safety: `send_scpi` v0.2 design + +`send_scpi` ships in v0.2 (see tool table above) and stays behind the `--allow-raw-scpi` startup flag. This subsection specs the design now so the v0.1 work on the structured-error contract (next subsection) lands a foundation the v0.2 implementation can plug straight into, and so reviewers can debate the safety model before we commit code. + +The risk being managed is twofold: (1) destructive operations the agent shouldn't issue without explicit user intent; (2) plausibly-correct-looking SCPI that the LLM hallucinated and that *appears* to succeed at the device because the firmware just returns an error string the agent ignores. Two-tier safety net + the structured-error contract handle both: + +**Tier 1: destructive deny-list (hard block)** — patterns refused unless the caller passes `confirmed: true` on that specific call. Initial list (case-insensitive on alpha; SCPI capital-letter abbreviation rules respected): + +- `SD:FORmat` / `SYST:STOR:SD:FORmat` +- `SYST:FORceBoot` / `SYST:FORC*` +- `*RST` (full system reset) +- `SYST:REBoot` (deny unless `confirmed=true` — many legitimate uses) +- `CONF:DAC:SAVEcal` / `CONF:ADC:SAVEcal` (calibration NVM writes) +- `LAN:FACReset`, `LAN:FWUpdate` + +Extensible via `--deny-scpi-pattern ` startup flag. The list is intentionally small — it covers "rm -rf" class operations, not every command that mutates state. + +**Tier 2: wiki-pattern warn (soft signal — hallucination check)** — at startup, parse a bundled snapshot of `01-SCPI-Interface.md` from the [`daqifi-nyquist-firmware.wiki`](https://github.com/daqifi/daqifi-nyquist-firmware/wiki/01-SCPI-Interface) repo, extract every published pattern, and pre-compute a canonical-form lookup table that respects the SCPI capital-letter abbreviation rules (`SYSTem:STReam:START` matches `SYST:STR:START`, `SYST:STR:STA`, `SYSTEM:STREAM:START`, etc.). + +At dispatch time, if a raw command does NOT canonicalize to any published pattern, the MCP returns a structured warning whose copy explicitly invites the LLM to self-check for hallucination: + +```jsonc +{ + "success": true, + "response": "", + "errors": [], + "warnings": [{ + "code": "UNDOCUMENTED_SCPI", + "message": "'SYST:FOO:BAR' isn't in the SCPI wiki (snapshot 2026-05-27). + LLMs sometimes invent plausible-looking SCPI — verify the syntax before + trusting the device response. If you believe the command is real + (undocumented or post-snapshot) proceed; otherwise try a suggestion below.", + "suggestions": ["SYST:FOO:BAZ", "SYST:FOO:BARQ?"] // Damerau-Levenshtein ≤ 3 + }] +} +``` + +**Why this exact phrasing matters:** "you may have hallucinated" is the key prompt. Without it, the LLM treats the warning as bureaucratic noise; with it, the model is invited to introspect on its own reliability for this specific command. The warning is non-blocking — the firmware's own response is the ground truth. The wiki may lag firmware (per the daqifi-nyquist-firmware CLAUDE.md: "When you add or modify SCPI commands, update the wiki same-day"), so an unmatched command isn't automatically wrong; it's a request for the LLM to double-check itself before reading too much into the response. + +**Wiki snapshot freshness:** the snapshot is bundled at build time via `scripts/refresh-scpi-wiki.sh` and the CI freshness check (snapshot must be ≤30 days old) both land in v0.1 under [#mcp-13](GITHUB_ISSUES.md#mcp-13) so the foundation is in place before v0.2 consumes it. Snapshot date is exposed in every warning so users can spot staleness even if CI is lenient. + +**Mode interaction** (per the safety modes above): +- Server must be launched with `--allow-raw-scpi` regardless of mode +- `read-only` → `send_scpi` blocked unconditionally even with `--allow-raw-scpi` +- `control` → deny-list active; `confirmed=true` overrides on a per-call basis; wiki-warn always on +- `admin` → same as control (deny-list does NOT auto-relax — `confirmed=true` per call is still required for destructive patterns; that's the explicit confirmation the RFC requires for admin-mode destructive tools) + +#### Structured error response contract (all SCPI tools — v0.1 work) + +The error contract below applies to every typed SCPI tool in v0.1 (`configure_analog_channel`, `start_streaming`, `start_sd_logging`, etc.) as well as the deferred `send_scpi`. It's listed here because it's the foundation the `send_scpi` safety design plugs into, and because shipping typed-tool errors in this structured shape from v0.1 means we don't have a contract break when v0.2 lands. + +Every SCPI tool — typed and raw — returns errors in a uniform shape so the model never has to grep free text to know whether something failed. Three error surfaces are scraped per call: + +1. **Synchronous SCPI error queue (`SYST:ERR?`)** — drained BEFORE the command (clear baseline), then drained AFTER (return the delta). Catches `-101 Invalid character`, `-200 Execution error`, etc. + +2. **Asynchronous LOG_E buffer (`SYST:LOG?`)** — opt-in via `scrapeLogAfter` (default `true` for `send_scpi`, default `false` for typed tools to avoid log-buffer drain on every call). After the command, drain the log buffer and return any `[ERROR]` / `[WARN]` lines that appeared during the call window. Note `SYST:LOG?` is destructive (clears the buffer). + +3. **Readback failures** — for tools with a `ReadbackAsync` validator (e.g. `configure_wifi` → APPLY → poll `LAN:ADDR?` until non-zero or timeout), failure to validate within the timeout returns a `ReadbackFailedError`. + +Response shape: + +```jsonc +{ + "success": true, // false iff any class-1 error + "response": "", // raw text or parsed value + "errors": [ + { "source": "SCPI", "code": -113, "message": "Undefined header" }, + { "source": "LOG_E", "code": null, "message": "[WIFI] Connection lost" }, + { "source": "READBACK", "code": null, "message": "LAN:ADDR? returned 0.0.0.0 after 20s" } + ], + "warnings": [ /* UNDOCUMENTED_SCPI etc. */ ] +} +``` + +Rationale: a single SCPI command can affect multiple subsystems and generate errors that arrive asynchronously after the immediate response returns "OK". An MCP that returns only the immediate response would miss the `LOG_E` that fires 200 ms later when the WINC driver rejects the new SSID. The model can't act on errors it doesn't see. + ### Distribution Three artifacts, one codebase, one CI job: @@ -221,13 +303,15 @@ Issues are enumerated in [`GITHUB_ISSUES.md`](GITHUB_ISSUES.md). Summary: 11. Distribution: AOT binary + `dotnet tool` + `npx` shim ([#mcp-11](GITHUB_ISSUES.md#mcp-11)) 12. Documentation: README rewrite + agent recipe guide ([#mcp-12](GITHUB_ISSUES.md#mcp-12)) -### v0.2 — Power features and gated ops (~3–4 weeks) +### v0.2 — Power features and gated ops (target: fast-follow, ~3–4 weeks after v0.1) + +**Priority:** ship v0.2 quickly so the MCP itself becomes the primary harness for exercising new firmware features. The DAQiFi firmware team currently types every new SCPI command into a Python test harness within hours of landing it; with `send_scpi` available behind the v0.2 safety net, that workflow moves entirely to Claude + the MCP and stops requiring a custom Python script per feature. - `wait_for_condition` (event-style waiting for agents) - Destructive SD ops (`delete_sd_file`, `format_sd_card`) behind admin mode - Network reconfiguration (`configure_wifi`, `configure_static_ip`) - Firmware update tools (`check_firmware_update`, `update_firmware`) -- `send_scpi` escape hatch behind `--allow-raw-scpi` +- `send_scpi` escape hatch behind `--allow-raw-scpi` (design specced — see [§Safety: send_scpi v0.2 design](#safety-send_scpi-v02-design)) - HTTP transport for hosted use cases ### v0.3+ — Ecosystem and platforms @@ -235,6 +319,7 @@ Issues are enumerated in [`GITHUB_ISSUES.md`](GITHUB_ISSUES.md). Summary: - Python client package (`pip install daqifi`) with both async API and MCP-over-IPC option - LangChain / LlamaIndex tool wrappers (auto-generated from the MCP surface) - Cloud deployment story for fleet monitoring +- **LabVIEW-as-client example library** — a small set of LabVIEW VIs that show how to invoke the DAQiFi MCP from inside a LabVIEW dataflow graph (see [§Q6](#q6-labview-interop) for the framing). Narrow scope, no per-VI support burden. - Agent-managed recipe library (saved configurations, parameterized) --- @@ -269,18 +354,234 @@ The MCP is the wedge; the marketing is what turns the wedge into a position. --- +## Firmware-aware constraints + +Items that aren't in the firmware README but ARE in `daqifi-nyquist-firmware`'s root `CLAUDE.md` / project memory. Tyler's RFC didn't cover these because they're firmware-team knowledge; the MCP needs to enforce them so the LLM doesn't trip them silently. + +### Quiescence rule — no SCPI during a benchmarked stream + +The firmware has a documented constraint (`CLAUDE.md` → "Quiescence Rule" + memory `feedback_no_scpi_during_benchmark`): **any SCPI query issued while a streaming session is in progress can throttle the encoder enough to corrupt the very measurement being collected.** Polling `STAT:QUES:COND?` at 1 Hz during a 3 kHz WiFi run silently turns `Wst=6500` (real drops) into `Wst=0` (the encoder yielded so often it didn't generate the drops). Out-of-band visibility (Saleae, Wireshark, PC iperf2 log) is the only safe inspection during a stream. + +This is the #1 footgun for an LLM driving the device. Without enforcement, an agent reasoning "I'll just check on the stream every few seconds" will trash benchmark integrity *and not know it*. + +**MCP enforcement:** + +- `DeviceRegistry` tracks `isStreaming` per handle (set by `start_streaming`, cleared by `stop_streaming` / disconnect / auto-stop). +- A `RequiresQuiescence` attribute on tool methods causes the dispatcher to return a structured `quiescence_violation` error when called against a streaming handle. Default-on for every tool that issues SCPI; `stop_streaming` / `disconnect_device` / `get_stream_summary` / `read_stream_window` are the allowlisted exceptions (they read in-process ring-buffer state, not the device). +- The error message tells the model exactly what to do: *"Cannot run `get_device_status` while `` is streaming — it would invalidate the measurement (see firmware quiescence rule). Use `get_stream_summary` for in-process state, or call `stop_streaming` first."* +- Opt-out via `respectQuiescence: false` for tools that need to poll the device mid-stream for a legitimate reason (rare; mostly debugging). The opt-out is logged in the audit trail. + +This is one of those rules that's much cheaper to enforce mechanically in v0.1 than to retrofit after the first "but my numbers were clean!" support ticket. + +### Variant-aware defaults and capability-filtered tool surface + +NQ1, NQ2, NQ3 differ in ADC resolution, channel counts, available peripherals (DAC on NQ3 only), and sensible default `voltagePrecision`. Tyler's tool table is variant-neutral; without per-variant smarts, the LLM has to know the differences and the user has to spell them out. + +- `connect_device` caches a `BoardCapabilities` blob per handle (read from the firmware at connect time via `*IDN?` + `SYST:SYSInfoPB?`). +- `start_streaming` etc. consult that blob and apply sane defaults: + - NQ1: `format=csv`, `voltagePrecision=4` (12-bit MC12bADC) + - NQ3: `format=csv`, `voltagePrecision=6` (18-bit AD7609) + - NQ2: `format=csv`, `voltagePrecision=7` (24-bit AD7173) +- DAC tools (v0.2) are filtered out of `list_tools` when the connected handle is an NQ1, or return a structured `not_supported_on_this_variant` error rather than producing wedged state. Same pattern for any future variant-specific tools. +- The capabilities are exposed as a resource (`daqifi://devices/{id}/capabilities`) — the authoritative answer to "what can this device do". + +The LLM gets to write *"start streaming"* and have it Just Work on whichever board is connected, instead of *"start streaming with the right format for whichever ADC you have"*. + +### ADC channel-enable: bitmask vs per-channel calls + +Subtle firmware-side perf trap: enabling channels one at a time via `ENABle:VOLTage:DC ,1` triggers the firmware's frequency-cap recompute once per call. Enabling 16 channels individually causes 16 recomputes (each one walking the channel-config table). The firmware also exposes a bitmask SCPI form that enables N channels with one recompute. + +`configure_analog_channel` for a single channel is fine. But `set_active_channels(channels: int[])` should issue the bitmask form (one SCPI call) rather than looping over `configure_analog_channel`. The model thinks "set channels 0,3,4,7,12" once; the device sees one mask write, not five. + +Worth specifying in the tool contract so the v0.1 implementation doesn't accidentally do the looped form. + +### Credentials handling — no echo, no audit + +WiFi passwords passed to `configure_wifi` are accepted as inputs but **never** appear in tool responses, audit logs, or error messages. This is a direct response to a 2026-05-07 incident recorded in project memory: the project leaked a real Tesla AP password from `batch.sh` that echoed substituted env vars during a debug session. + +`ScpiAuditLog` (see below) masks any argument whose key name contains `pass`/`secret`/`key`/`token`, replacing the value with `***REDACTED***`. The masking is applied at log-emission time, not at input time — the password is still usable for the SCPI call. + +--- + +## Cross-cutting infrastructure (v0.1 internal) + +### ScpiAuditLog + +Every SCPI command sent (with arguments masked for sensitive keys) and every response received is appended to a session-scoped audit log, retrievable via the resource `daqifi://session/audit`. This is what the model and the human read together when *"the WiFi configure didn't work"* and we need to retro-debug. Bounded (last 500 entries) to keep memory in check. The audit log also captures `quiescence_violation` opt-outs so we can see when the user explicitly bypassed the safety rail. + +### RateLimiter + +Per-handle rate limit (e.g. no more than 30 tool calls per second) prevents a runaway LLM loop from hammering the device. The firmware can handle bursts but we don't need the model to discover that the hard way — and rate-limit errors are a useful signal that the agent is in a degenerate loop. + +### Logging conventions + +Server writes structured logs (`Microsoft.Extensions.Logging`) to stderr — stdout is reserved for JSON-RPC and must stay clean. The MCP client (Claude Desktop, Cursor, Cline) typically tails stderr. + +### Telemetry (opt-in) + +Per [Q4 resolution](#q4-telemetry), the server can collect anonymous usage telemetry to help us learn what agents actually call and where pain points cluster. **Off by default.** Enable via `--telemetry` flag or `DAQIFI_MCP_TELEMETRY=1` env var. + +**What's collected per tool call:** + +| Field | Example | Why | +|---|---|---| +| `tool` | `"start_streaming"` | Which surfaces get used | +| `mode` | `"control"` | Which safety posture users actually pick | +| `outcome` | `"ok"` / `"scpi_error"` / `"quiescence_violation"` / `"timeout"` | Where failures cluster | +| `error_code` | `-200` (SCPI) or `null` | Group similar failures | +| `latency_ms` | `342` | Tail-latency outliers reveal device-side bottlenecks | +| `server_version` | `"0.1.0"` | Cross-version regression detection | +| `client_id` | `"claude-desktop/1.x"` (from MCP InitializeResult) | Which client ecosystems we serve | +| `os` | `"linux"` / `"win"` / `"mac"` | Platform skew | +| `device_variant` | `"NQ1"` / `"NQ2"` / `"NQ3"` | Variant-specific issue detection | +| `session_id` | random UUID, server-process-scoped | Sequence reconstruction within a session | +| `event_ts` | epoch ms | Time-bucketing for retention | + +**What's EXPLICITLY NOT collected:** + +- Tool *arguments* (channel numbers, frequencies, file paths, SCPI strings — too easy to leak research-sensitive setups) +- Tool *responses* / SCPI response text +- Device serials, MAC addresses, IP addresses, hostnames +- WiFi SSIDs, passwords, any credential +- File contents or filenames (the SD card might hold a customer's pre-clinical data) +- The user's `client_id` beyond the tool name (no per-user identifier; `session_id` is server-process-scoped and resets each launch) + +**Wire shape:** batched HTTP POST to a daqifi.com endpoint (TBD with Tyler — likely a CloudFlare worker + bucket setup, cheap and easy). Batches of up to 100 events flushed every 60 s or on shutdown. Failed flushes drop silently (we don't degrade the user's session for telemetry). + +**Disclosure & inspection:** + +- README has a dedicated **Telemetry** section listing every field collected, where it goes, and how to disable +- `--telemetry-dry-run` mode prints every event that *would* be sent to stderr instead of sending — lets a paranoid user (or a procurement reviewer) verify exactly what we collect before opting in +- Server logs an `INFO` line on startup when telemetry is enabled: `Telemetry enabled — see README §Telemetry for details. Disable with --no-telemetry.` + +**Retention:** 90 days raw event-level; indefinite as anonymized aggregates (counts, percentiles). Public dashboard at some later date if/when the data is interesting enough to be community-useful. + +**Compliance:** the data shape is engineered specifically to fall below common compliance triggers — no identifiers, no contents, no metadata that could re-identify a specific user / lab / experiment. We don't sell, share with third parties, or use it for marketing personalization. Reviewers can verify all of this by reading the open-source server code and running `--telemetry-dry-run`. + +--- + +## Cross-cutting concern for daqifi-core + +`daqifi-core` already does most of what the MCP needs, but two small API additions would clean up the MCP layer and also benefit `daqifi-desktop`. Neither blocks v0.1; both are mentioned so they can be batched into a single follow-up PR if Tyler agrees. + +### 1. `Task GetMetadataAsync()` on `DaqifiDevice` + +The serial number, IP, part number, and firmware version arrive in the first protobuf info message after connect, but they aren't surfaced as typed properties until the consumer subscribes to events and processes the message. Today the MCP would have to: + +```csharp +var tcs = new TaskCompletionSource(); +device.ChannelsPopulated += (s, e) => tcs.TrySetResult(BuildMetadata(device)); +device.Connect(); +var metadata = await tcs.Task.WaitAsync(TimeSpan.FromSeconds(5)); +``` + +A typed `GetMetadataAsync(CancellationToken)` that returns once the first protobuf info has been parsed (or throws on timeout) would replace that boilerplate with one line. Internal implementation can keep the event subscription pattern; the public surface is just cleaner. + +### 2. Public `ExecuteConfirmedAsync(command, drainErrorQueue = true)` + +`ExecuteTextCommandAsync` and `DrainErrorQueueAsync` are both `protected` instance methods on `DaqifiDevice`. The MCP's "send a SCPI command and tell me what errors it produced" wrapper needs both. Today we'd either (a) subclass `DaqifiDevice` just to expose them or (b) re-implement the SCPI text channel from scratch. Both are ugly. + +Proposed signature: + +```csharp +public Task ExecuteConfirmedAsync( + string command, + bool drainErrorQueue = true, + bool scrapeLogAfter = false, + CancellationToken ct = default); + +public record ConfirmedScpiResult( + string ResponseText, + IReadOnlyList SyncErrors, + IReadOnlyList AsyncLogLines); +``` + +This is the building block for the [structured error response contract](#structured-error-response-contract-all-scpi-tools--v01-work) above. `daqifi-desktop` would also benefit — it currently reproduces parts of this pattern for its own SCPI-console feature. + +--- + ## Open questions -These need a decision before or during v0.1. Each is a candidate for a thread on the RFC PR. +Resolved on 2026-05-27 in discussion between Chris (hardware/firmware partner) and Claude. Tyler should override any of these in PR review if his view differs. + +### Q1. Buyer segmentation + +> AI-native marketing pulls greenfield buyers. Migrators (existing LabVIEW shops) won't switch on the AI story alone. Do we accept that, or invest in migration tooling? + +**Answered (Chris, 2026-05-27):** Accept it; keep it light. NO native LabVIEW migration tool — the support burden of every customer's bespoke VI is permanent. Paid migration *assists* are fine on a per-engagement basis. For self-serve migration, LabVIEW users can lean on the DAQiFi MCP + Claude Code / their CLI of choice to walk them through translating VIs into MCP-driven workflows. We may publish examples eventually; not for v0.1. + +(Related: Q6 — different direction. We *will* think about letting LabVIEW be a *client* of the MCP. See below.) + +### Q2. Pricing strategy + +> Does the AI story support a higher ASP, a SaaS tier (managed recipes, hosted dashboard), or neither? This needs to be decided *before* the launch copy is written. + +**Answered (Chris, 2026-05-27):** MCP alone does not move pricing. A future SaaS offer (managed recipes, hosted dashboard, fleet management) likely would — defer that pricing decision until the SaaS scope is real. + +### Q3. Agent action boundaries + +> Is the default "control" mode the right safety posture, or do we ship "read-only" as default and require an opt-in flag for control? + +**Answered (Chris, 2026-05-27):** `--mode=control` stays the default. Control is the most powerful feature and the whole pitch — making users opt in via a flag every time would kill the conversational magic in the launch demos. The destructive deny-list + per-call `confirm: true` + `--max-sample-rate-hz` / `--max-voltage-range` clamps are the mitigations; nothing in v0.1's control surface can permanently damage anything. Destructive ops (firmware update, SD format, factory reset) are all v0.2 and require admin mode + explicit per-call confirmation. + +`--mode=read-only` remains explicit for CI / automated contexts where absolute certainty of no state change matters. + +### Q4. Telemetry + +> Do we want anonymous usage telemetry from the MCP server to learn what agents actually call? If yes, opt-in only, and stated clearly in the README. + +**Answered (Chris, 2026-05-27):** Yes — telemetry is desired for product development and pain-point detection. **Strict opt-in** (off by default; user passes `--telemetry` or sets `DAQIFI_MCP_TELEMETRY=1`). README discloses exactly what's collected. See [§Telemetry (opt-in)](#telemetry-opt-in) below for the data shape and design. + +### Q5. MCP stability contract + +> What is our promise to users about tool name/schema stability? + +**Answered (Chris, 2026-05-27):** Tyler's framing is right, tightened as follows: + +- **Tool names** are stable from v0.1.0 onwards. Renaming a tool is a breaking change requiring a major-version bump. +- **Parameters**: *optional* fields may be added freely at any version. *Required* fields may not be added later; if a new required field is needed, ship it as optional in vN and required in v(N+1) with a deprecation window. +- **Tool removal** is the only "remove things" breaking change. Requires (a) one minor release with a `deprecated: true` schema flag + runtime warning, (b) next major bumps. Minimum **6 months** between deprecation announcement and removal — slow enough for a slow-moving lab user, fast enough that we don't carry dead code forever. +- **`experimental: true`** schemas (e.g. `send_scpi` in v0.2) carry NO stability promise. Their behavior, parameters, and existence may change in any minor release. Clearly flagged in the tool description. +- **Resource URIs** (`daqifi://...`) follow the same rules as tool names. + +This mirrors the gRPC / Anthropic API / MCP SDK stability conventions — proven, low-surprise. + +### Q6. LabVIEW interop + +> Is there a story where the MCP can drive a LabVIEW VI as a tool call? Probably v0.3+, but flagging now because it could be a marketing weapon against NI. + +**Answered (Chris, 2026-05-27):** **Inverted from the original question.** Instead of MCP-calls-out-to-LabVIEW (which carries permanent per-VI support burden), the more interesting direction is MCP-as-server-with-LabVIEW-as-client: + +- A LabVIEW user drops a `DAQiFi MCP Tool Call.vi` (or similar) into their dataflow graph +- The VI internally opens a JSON-RPC connection to the DAQiFi MCP server (via stdio launch or local HTTP transport) +- LabVIEW dataflow inputs/outputs marshal to/from MCP tool call params/results +- LabVIEW users get programmatic device access without LabVIEW-side custom code, AND can keep using Claude / Cursor / Cline alongside + +This is much narrower: a small example library (a few VIs + a brief README) rather than a featureful product. No support burden because the LabVIEW side is just a JSON-RPC client — we don't own any customer VIs. + +Defer to v0.3+ at the earliest. For v0.1/v0.2, the marketing weapon is the published recipe + blog post showing *"I had Claude drive both DAQiFi and a legacy LabVIEW rig in the same conversation."* + +### Q7. Hosted recipe library + +> Recipes as MCP prompts ship in the binary. Do we also stand up a hosted directory where users contribute and rate recipes? Network effect potential, but support burden. + +**Answered (Chris, 2026-05-27):** Three-stage rollout based on actual demand: + +- **v0.1**: 5 in-binary recipes (Tyler's list). Discovery is "look in the MCP prompts list" + "read the README." +- **v0.2** (only if contribution requests appear): add a `recipes/` directory in this repo; point users at GitHub Discussions for sharing. Free, no support burden, GitHub-native contribution flow. Track: how often does it get used? +- **v0.3+** (only if `recipes/` proves itself): formalize a hosted directory (web app, search, ratings, parameterized prompts). Real eng cost + ongoing moderation burden — build it only when the flywheel has proven itself. + +Building a hosted library in v0.1 would be infrastructure for a community that doesn't exist yet. + +### Q8. DAQiFi Desktop alignment + +> Does Desktop adopt the MCP internally as its control layer, or stay separate? + +**Answered (Chris, 2026-05-27):** Stay separate for v0.1. Desktop and MCP both depend on `Daqifi.Core` directly; no coupling between them. + +In v0.2, consider piloting **one Desktop feature** on top of the MCP — the SCPI Console is the natural candidate, since it benefits from the same wiki-warn + structured-error contract + audit log we're building. Low risk (developer-tool surface, not core acquisition path), high signal (we learn whether the contracts hold up under a non-LLM client). Evaluate broader migration in v0.3+ based on that pilot. -1. **Buyer segmentation.** AI-native marketing pulls greenfield buyers. Migrators (existing LabVIEW shops) won't switch on the AI story alone. Do we accept that, or invest in migration tooling? -2. **Pricing strategy.** Does the AI story support a higher ASP, a SaaS tier (managed recipes, hosted dashboard), or neither? This needs to be decided *before* the launch copy is written. -3. **Agent action boundaries.** Is the default "control" mode the right safety posture, or do we ship "read-only" as default and require an opt-in flag for control? The conservative choice protects us from launch-week horror stories. -4. **Telemetry.** Do we want anonymous usage telemetry from the MCP server to learn what agents actually call? If yes, opt-in only, and stated clearly in the README. -5. **MCP stability contract.** What is our promise to users about tool name/schema stability? Suggest: tool *names* are stable from v0.1; tool *parameters* may add optional fields without notice; breaking changes bump major version. -6. **LabVIEW interop.** Is there a story where the MCP can drive a LabVIEW VI as a tool call? Probably v0.3+, but flagging now because it could be a marketing weapon against NI. -7. **Hosted recipe library vs. in-repo.** Recipes as MCP prompts ship in the binary. Do we also stand up a hosted directory where users contribute and rate recipes? Network effect potential, but support burden. -8. **DAQiFi Desktop alignment.** Does Desktop adopt the MCP internally as its control layer, or stay separate? Adopting it would force a clean API and double as dogfooding, but adds scope. +The "force a clean API" benefit Tyler cited is real, but we can get most of it from a single-feature pilot without committing the whole Desktop product to a contract we're still learning. ---