Skip to content

feat(mcp): MCP server for agent-driven DAQiFi device control#277

Merged
tylerkron merged 3 commits into
mainfrom
claude/unruffled-booth-7aadb3
Jun 30, 2026
Merged

feat(mcp): MCP server for agent-driven DAQiFi device control#277
tylerkron merged 3 commits into
mainfrom
claude/unruffled-booth-7aadb3

Conversation

@tylerkron

Copy link
Copy Markdown
Contributor

Summary

Adds Daqifi.Mcp, a Model Context Protocol server (stdio) that lets an AI agent — Claude Desktop/Code, Cursor, Codex, etc. — drive a DAQiFi Nyquist device through Daqifi.Core with no application code: discover → connect → configure analog channels + sample rate → start/stop on-device SD-card logging.

It's a thin facade over Daqifi.Core (in-process, no protocol re-implementation), distributed as a dotnet tool. Scope is intentionally the basic control loop; live data streaming and SD file list/download are deferred to follow-ups.

What's in it

  • src/Daqifi.Mcp — the server: a DaqifiAgent facade over the real Core surface (DaqifiDeviceFactory.ConnectFromDeviceInfoAsync, IStreamingDevice.EnableChannels, ISdCardOperations), 10 MCP tools, an stdio host with all logging routed to stderr (stdout is reserved for JSON-RPC).
  • src/Daqifi.Mcp.Tests — xUnit coverage of option parsing and agent guard rails.
  • release.yml — publishes the dotnet tool to NuGet on tagged releases (alongside the existing Daqifi.Core push).

Tools

discover_devices · connect_device · list_connected_devices · disconnect_device · get_device_status · list_channels · configure_analog_channels · set_sample_rate · start_sd_logging · stop_sd_logging

Notable design points

  • Faithful to the real Core API — channel config goes through IStreamingDevice.EnableChannels/DisableChannel (+ the ADC bitmask), not the per-channel value interfaces.
  • Concurrency-safe — connect/disconnect/configure/rate/logging are serialized behind a per-agent gate, since the transport can dispatch tool calls concurrently.
  • Clean lifecycle — graceful shutdown drains connected devices so serial ports are released (and any in-progress SD capture is stopped) instead of being left held.
  • Honest contracts — sample rate is clamped to the 1000 Hz hardware ceiling and the result reports the clamp; start_sd_logging returns the real on-card filename; errors surface Core's human-readable messages so the agent can self-correct.
  • Safety flags--read-only (block mutations) and --max-sample-rate-hz.

How to run it

dotnet tool install -g Daqifi.Mcp     # exposes the `daqifi-mcp` command

Claude Desktop (claude_desktop_config.json):

{ "mcpServers": { "daqifi": { "command": "daqifi-mcp", "args": [] } } }

Cursor (~/.cursor/mcp.json) is the same shape; Codex uses [mcp_servers.daqifi] in ~/.codex/config.toml. See src/Daqifi.Mcp/README.md.

Test plan / verification

  • ✅ Solution builds in Release, 0 warnings; 11/11 unit tests pass
  • ✅ MCP protocol smoke test: initialize + tools/list return all 10 tools with correct schemas
  • Live end-to-end on real hardware (Nyquist1, FW 3.6.2, USB): discover → connect → configure analog [0,1,2,3] → set rate → start_sd_logging (loggingToSdCard:true confirmed) → stop_sd_logging → disconnect
  • ✅ Sample-rate clamp verified live: 5000 → 1000, clamped:true with an explanatory note
  • start_sd_logging returns the real filename (log_<timestamp>.bin)
  • Port-release test: connect then abruptly close the client (no disconnect_device) → a fresh server immediately reconnects (serial port released, not leaked)

Out of scope (follow-ups)

  • list_sd_files / download_sd_file (so the agent can see/retrieve what it logged)
  • Live streaming evolution (host ring buffer + get_channel_summary)
  • Read-only tool annotations / not registering mutating tools in --read-only mode

🤖 Generated with Claude Code

Add Daqifi.Mcp, a Model Context Protocol server (stdio) that lets an AI
agent (Claude, Cursor, Codex, ...) drive a DAQiFi Nyquist device through
Daqifi.Core: discover, connect, configure analog channels + sample rate,
and run on-device SD-card logging. Distributed as a `dotnet tool`.

Scope is intentionally the basic control loop; live streaming and SD file
list/download are deferred to follow-ups.

- 10 tools over a thin DaqifiAgent facade wrapping the real Core surface
  (DaqifiDeviceFactory, IStreamingDevice channel APIs, ISdCardOperations)
- stdio host with all logging routed to stderr so it cannot corrupt the
  JSON-RPC stream
- per-agent serialization of device mutations; graceful shutdown drains
  connected devices so serial ports are released on exit
- sample rate clamped to the 1000 Hz hardware ceiling; --read-only and
  --max-sample-rate-hz flags
- xUnit tests; release.yml publishes the dotnet tool on tagged releases

Validated end-to-end against a real Nyquist1 (firmware 3.6.2) over USB.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tylerkron tylerkron requested a review from a team as a code owner June 30, 2026 21:08
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add MCP stdio server + dotnet tool to control DAQiFi devices via Daqifi.Core

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add Daqifi.Mcp MCP (stdio) server exposing DAQiFi discovery, connect, configure, and SD logging
 tools.
• Implement concurrency-safe agent facade over Daqifi.Core with shutdown cleanup and safety flags.
• Add unit tests and update release workflow to publish the new dotnet tool.
Diagram

graph TD
  A["MCP Client (Claude/Cursor/etc)"] --> B["stdio JSON-RPC"] --> C["Daqifi.Mcp host"] --> D["DaqifiTools (10 tools)"] --> E["DaqifiAgent"] --> F["Daqifi.Core SDK"] --> G["Nyquist device"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Conditionally register mutating tools in --read-only mode
  • ➕ Makes read-only mode self-describing in tools/list (no “call succeeds but is blocked”).
  • ➕ Reduces runtime guard paths and associated test surface.
  • ➖ Requires dynamic tool registration or splitting tools into read-only vs mutating assemblies/types.
  • ➖ Slightly more complexity in server composition vs a single Guard/RequireControl check.
2. Move shutdown draining to IHostApplicationLifetime / IHostedService
  • ➕ Centralizes lifecycle cleanup and runs on multiple shutdown paths (SIGTERM, host stop).
  • ➕ Avoids the subtle “resolve agent before RunAsync” pattern.
  • ➖ Adds another hosting component and slightly more boilerplate.
  • ➖ The current finally-based approach is already effective for stdio client disconnects.
3. Use System.CommandLine (or similar) for option parsing
  • ➕ More robust parsing/validation and better help/usage output.
  • ➕ Easier future expansion of flags without manual scanning.
  • ➖ Additional dependency and startup complexity for a very small flag set.
  • ➖ Host builder args integration still needs care due to value-less switches.

Recommendation: Keep the current thin-facade approach: it preserves Daqifi.Core semantics, keeps the MCP layer minimal, and the per-agent serialization is a pragmatic safety choice for concurrent tool dispatch. Consider conditional tool registration for --read-only in a follow-up if clients rely heavily on tools/list to infer capabilities; otherwise the current guardrails are sufficient.

Files changed (11) +1010 / -5

Enhancement (6) +747 / -0
Daqifi.Mcp.csprojAdd Daqifi.Mcp dotnet tool project +37/-0

Add Daqifi.Mcp dotnet tool project

• Creates a net9.0 executable packaged as a global dotnet tool (daqifi-mcp). Adds dependencies on ModelContextProtocol and Microsoft.Extensions.Hosting, and packs README.md into the NuGet package.

src/Daqifi.Mcp/Daqifi.Mcp.csproj

DaqifiAgent.csImplement concurrency-safe facade over Daqifi.Core device control +374/-0

Implement concurrency-safe facade over Daqifi.Core device control

• Implements discovery, connect/disconnect, status/channel introspection, analog-channel configuration, sample-rate clamping, and SD logging start/stop over Daqifi.Core APIs. Serializes mutating operations behind a semaphore and provides best-effort shutdown draining to release ports and stop SD logging.

src/Daqifi.Mcp/DaqifiAgent.cs

Dtos.csDefine MCP-facing DTOs for devices, channels, and operation results +96/-0

Define MCP-facing DTOs for devices, channels, and operation results

• Adds records for discovered/connected device info, live status snapshots, channel info, and results for configuration, sample rate changes, and SD logging start. DTOs translate Daqifi.Core objects into stable tool outputs.

src/Daqifi.Mcp/Dtos.cs

Program.csWire up stdio MCP host with stderr-only logging +46/-0

Wire up stdio MCP host with stderr-only logging

• Bootstraps a Microsoft.Extensions.Hosting app, parses custom CLI flags, and configures MCP stdio transport with tool discovery from the assembly. Forces all logging to stderr to avoid corrupting the JSON-RPC stream and drains devices on shutdown.

src/Daqifi.Mcp/Program.cs

ServerOptions.csAdd CLI options for read-only mode and sample-rate cap +58/-0

Add CLI options for read-only mode and sample-rate cap

• Implements simple flag parsing for --read-only and --max-sample-rate-hz and provides a help text block. Options are used to gate mutating operations and clamp set_sample_rate requests.

src/Daqifi.Mcp/ServerOptions.cs

DaqifiTools.csExpose 10 MCP tools over DaqifiAgent with error surfacing +136/-0

Expose 10 MCP tools over DaqifiAgent with error surfacing

• Defines MCP tool methods for discovery, connection management, status/channel listing, analog config, sample rate changes, and SD logging control. Wraps calls in Guard helpers that convert exceptions into McpException messages for agent-friendly feedback.

src/Daqifi.Mcp/Tools/DaqifiTools.cs

Tests (2) +114 / -0
Daqifi.Mcp.Tests.csprojCreate MCP server test project +25/-0

Create MCP server test project

• Introduces a new xUnit test project targeting net9.0 with warnings-as-errors. References Daqifi.Mcp and the required test SDK packages.

src/Daqifi.Mcp.Tests/Daqifi.Mcp.Tests.csproj

DaqifiMcpTests.csAdd unit tests for CLI options and agent guardrails +89/-0

Add unit tests for CLI options and agent guardrails

• Covers ServerOptions parsing defaults and flags. Adds DaqifiAgent tests for error messaging, unknown device handling, sample-rate validation, and read-only blocking behavior.

src/Daqifi.Mcp.Tests/DaqifiMcpTests.cs

Documentation (1) +88 / -0
README.mdDocument installation and client configuration for the MCP server +88/-0

Document installation and client configuration for the MCP server

• Adds usage docs covering available tools, install/run modes, flags, and setup for Claude Desktop/Code, Cursor, and Codex. Highlights stdout/stderr constraints and the thin-wrapper relationship to Daqifi.Core.

src/Daqifi.Mcp/README.md

Other (2) +61 / -5
release.ymlPublish Daqifi.Mcp tool package alongside Daqifi.Core +8/-2

Publish Daqifi.Mcp tool package alongside Daqifi.Core

• Updates the release workflow to pack and push a new Daqifi.Mcp NuGet package (dotnet tool). Also narrows the Daqifi.Core push glob to only publish Core packages explicitly.

.github/workflows/release.yml

Daqifi.Core.slnAdd Daqifi.Mcp projects and additional build platforms +53/-3

Add Daqifi.Mcp projects and additional build platforms

• Adds the new Daqifi.Mcp and Daqifi.Mcp.Tests projects to the solution. Expands solution configurations to include x64/x86 variants and wires project build mappings.

Daqifi.Core.sln

@qodo-code-review

qodo-code-review Bot commented Jun 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Invalid argument ordering ✓ Resolved 🐞 Bug ≡ Correctness
Description
DaqifiAgent.ConnectAsync passes a positional argument after a named argument to
ConnectFromDeviceInfoAsync, which is illegal C# and fails compilation.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R90-92]

+            var device = await DaqifiDeviceFactory
+                .ConnectFromDeviceInfoAsync(info, options: null, cancellationToken)
+                .ConfigureAwait(false);
Relevance

⭐⭐⭐ High

Team previously fixed source-breaking argument/parameter issues; likely to accept compile-fix for
argument ordering.

PR-#198

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR calls ConnectFromDeviceInfoAsync using a named second argument followed by an unnamed third
argument; the Core method signature confirms there are three parameters and requires correct
argument passing.

src/Daqifi.Mcp/DaqifiAgent.cs[90-92]
src/Daqifi.Core/Device/DaqifiDeviceFactory.cs[222-226]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ConnectFromDeviceInfoAsync` is called with a named argument (`options:`) followed by a positional argument (`cancellationToken`), which is not allowed in C# and will prevent the project from compiling.

### Issue Context
`DaqifiDeviceFactory.ConnectFromDeviceInfoAsync` has the signature `(IDeviceInfo deviceInfo, DeviceConnectionOptions? options = null, CancellationToken cancellationToken = default)`, so the third argument must be either positional *before* any named args, or also named.

### Fix
Change the call to either fully positional:
- `ConnectFromDeviceInfoAsync(info, null, cancellationToken)`

or keep naming but name the token too:
- `ConnectFromDeviceInfoAsync(info, options: null, cancellationToken: cancellationToken)`

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[90-92]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Racy channel enumeration ✓ Resolved 🐞 Bug ☼ Reliability
Description
The MCP layer snapshots channels by enumerating DaqifiDevice.Channels without the Core channels
lock, so concurrent channel repopulation can throw InvalidOperationException during tool calls.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R339-340]

+    /// <summary>Snapshots the live channel view so callers never fold it while the consumer thread repopulates it.</summary>
+    private static IReadOnlyList<IChannel> Snapshot(DaqifiDevice device) => device.Channels.ToArray();
Relevance

⭐⭐⭐ High

Team accepted locking/snapshotting Channels to avoid concurrent mutation; MCP snapshot should follow
Core’s pattern.

PR-#250

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Core documents Channels as a live view and provides a lock-protected snapshot to avoid
concurrent-mutation InvalidOperationException; MCP enumerates Channels directly in multiple places,
and Core repopulates the list under a lock during status processing, making the race real.

src/Daqifi.Mcp/DaqifiAgent.cs[339-340]
src/Daqifi.Mcp/Dtos.cs[36-46]
src/Daqifi.Mcp/Dtos.cs[59-70]
src/Daqifi.Core/Device/DaqifiDevice.cs[53-71]
src/Daqifi.Core/Device/DaqifiDevice.cs[1132-1152]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`DaqifiDevice.Channels` is a live view over a list that Core mutates under a lock while processing status messages. The MCP layer calls `device.Channels.ToArray()` directly (including in DTO builders), which can race with list mutation and throw `InvalidOperationException` (or return inconsistent snapshots).

### Issue Context
Core explicitly documents that callers should use a lock-protected snapshot (currently `SnapshotChannels()` is protected) to avoid concurrent-mutation problems.

### Fix
Prefer adding a **public** snapshot API in `DaqifiDevice` (or another Core type) that returns a lock-protected array/list of channels, and use that everywhere in MCP instead of enumerating `Channels` directly.

For example:
- Add `public IReadOnlyList<IChannel> GetChannelsSnapshot()` in `DaqifiDevice` that locks `_channelsLock` and returns `_channels.ToArray()`.
- Update MCP’s `Snapshot(...)`, `ConnectedDeviceInfo.From(...)`, and `DeviceStatus.From(...)` to call that new API.

### Fix Focus Areas
- src/Daqifi.Mcp/DaqifiAgent.cs[339-346]
- src/Daqifi.Mcp/Dtos.cs[36-46]
- src/Daqifi.Mcp/Dtos.cs[59-70]
- src/Daqifi.Core/Device/DaqifiDevice.cs[53-71]
- src/Daqifi.Core/Device/DaqifiDevice.cs[1132-1152]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Core push lacks skip ✓ Resolved 🐞 Bug ☼ Reliability
Description
The release workflow pushes Daqifi.Core without --skip-duplicate, so rerunning a tagged release can
fail on already-published versions while MCP push is idempotent.
Code

.github/workflows/release.yml[R47-48]

    - name: Push to NuGet
-      run: dotnet nuget push nupkgs/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json 
+      run: dotnet nuget push nupkgs/Daqifi.Core.*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
Relevance

⭐⭐ Medium

No prior review history on release.yml; past workflow PRs didn’t address --skip-duplicate
idempotency.

PR-#175
PR-#259

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow shows the Core push step without --skip-duplicate and the MCP push step with it, making
the release job non-idempotent for Core.

.github/workflows/release.yml[44-54]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The workflow uses `--skip-duplicate` for `Daqifi.Mcp` but not for `Daqifi.Core`. If the release job is re-run after a partial success, the Core push can fail because the version already exists on NuGet.

### Fix
Add `--skip-duplicate` to the `Daqifi.Core` push step (matching MCP’s push behavior).

### Fix Focus Areas
- .github/workflows/release.yml[44-54]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Nonpositive max rate allowed ✓ Resolved 🐞 Bug ≡ Correctness
Description
--max-sample-rate-hz accepts 0/negative values and SetSampleRateAsync can apply a <=0 sample rate to
StreamingFrequency despite validating requested rate >= 1.
Code

src/Daqifi.Mcp/DaqifiAgent.cs[R190-196]

+            // The hardware ceiling (1000 Hz) always applies; --max-sample-rate-hz can only lower it.
+            var cap = _options.MaxSampleRateHz is { } max
+                ? Math.Min(HardwareMaxSampleRateHz, max)
+                : HardwareMaxSampleRateHz;
+
+            var applied = Math.Min(rateHz, cap);
+            streaming.StreamingFrequency = applied;
Relevance

⭐⭐ Medium

Repo often accepts bounds validation, but no historical evidence for validating --max-sample-rate-hz
specifically.

PR-#187
PR-#249

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR stores the parsed max rate without validating it, and SetSampleRateAsync uses it to compute
an applied rate; Core’s StreamingFrequency property has no validation, so invalid values can be set.

src/Daqifi.Mcp/ServerOptions.cs[24-36]
src/Daqifi.Mcp/DaqifiAgent.cs[177-201]
src/Daqifi.Core/Device/DaqifiStreamingDevice.cs[56-60]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ServerOptions.Parse` accepts any integer for `--max-sample-rate-hz`, including 0 and negative values. `SetSampleRateAsync` then computes `cap` from that value and does `applied = Math.Min(rateHz, cap)`, which can yield `applied <= 0` and assign it to `streaming.StreamingFrequency`.

### Issue Context
`rateHz` is validated to be `>= 1`, but the effective cap is not.

### Fix
Either:
- Reject/ignore non-positive CLI values in `ServerOptions.Parse` (e.g., only set `maxRate` when `rate >= 1`), and/or
- Clamp the computed `cap` in `SetSampleRateAsync` to at least 1 (and at most `HardwareMaxSampleRateHz`).

### Fix Focus Areas
- src/Daqifi.Mcp/ServerOptions.cs[24-36]
- src/Daqifi.Mcp/DaqifiAgent.cs[177-201]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread src/Daqifi.Mcp/DaqifiAgent.cs
Comment thread src/Daqifi.Mcp/DaqifiAgent.cs Outdated
Comment thread src/Daqifi.Mcp/DaqifiAgent.cs
Comment thread .github/workflows/release.yml Outdated
tylerkron and others added 2 commits June 30, 2026 15:23
…shot)

- ServerOptions: ignore non-positive --max-sample-rate-hz, and clamp the
  effective cap to [1, 1000] so a bad flag can't apply a <= 0 rate (Qodo #3)
- Add public DaqifiDevice.GetChannelsSnapshot() (lock-protected) and use it
  in the MCP layer instead of folding the live Channels view, closing the
  concurrent-repopulation race that .ToArray() only narrowed (Qodo #2)
- ConnectFromDeviceInfoAsync: use fully-positional args for clarity (Qodo #1;
  the named-then-positional form already compiled — CI was green — but this
  reads more clearly)
- release.yml: add --skip-duplicate to the Daqifi.Core push so re-running a
  tagged release is idempotent, matching the MCP push (Qodo #4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a short pointer to src/Daqifi.Mcp (intro line, "Where DAQiFi Core
fits" table row, and a maintainer note that releases also publish the
dotnet tool). Intentionally minimal — no product repositioning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tylerkron tylerkron merged commit 9b8a9ae into main Jun 30, 2026
1 check passed
@tylerkron tylerkron deleted the claude/unruffled-booth-7aadb3 branch June 30, 2026 22:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant