feat(tunnel): MCP read-only server tunnel mode (VA-26) #22

Merged
David merged 1 commit from feat/mcp-readonly-va-26 into main 2026-05-20 10:59:41 +02:00
Owner

Summary

  • Add USAGE_MCP=8 tunnel mode (crates/meshagent/src/host/mcp.rs) that exposes the agent's read-only host inventory to MCP (Model Context Protocol) clients reached through the rendezvous tunnel.
  • New meshagent-mcp crate carries a transport-agnostic JSON-RPC 2.0 server (handshake, tools/list, tools/call + notifications/progress, ping) speaking the current MCP spec revision (2025-11-25).
  • v1 mutating-tool gate: DENYLISTED_TOOL_NAMES + verify_read_only refuse to serve a registry that carries any destructive tool (exec, file_write, power_reboot, ...). Lifted in v2 (VA-27) behind a mutating-tools feature + the consent layer.
  • 17 read-only tools exposed: osinfo, cpuinfo, sysinfo, disks, users, process_list, dir_list, file_read, file_stat, services_list, packages_list, listen_ports, net_interfaces, dns_lookup, tcp_probe, time_status, log_tail (streaming).
  • log_tail demonstrates notifications/progress: one progress event per line addressed at the caller's _meta.progressToken, with the full result still returned so non-subscribing clients see complete output.

Deviations from the issue body

  • The VA-26 proposed-approach line called for rmcp (Anthropic's official Rust MCP SDK). I deferred that to v2. None of rmcp 1.7's built-in transports (stdio, Streamable HTTP) line up with the channel-backed tunnel transport without a duplex adapter, and the hand-rolled JSON-RPC is ~300 lines that we can swap for rmcp once the v2 consent layer forces typed schemas. Captured in the file-level doc on meshagent-mcp/src/lib.rs.

Acceptance criteria status

  • New meshagent-mcp crate compiles cleanly; cargo clippy --workspace --all-targets -- --deny warnings passes.
  • TunnelMode::Mcp (USAGE_MCP = 8) routed in host/tunnel.rs.
  • 17 read-only host modules exposed as MCP tools, each with a JSON Schema input descriptor.
  • log_tail emits MCP progress notifications and terminates cleanly on tunnel close (verified by mcp_tunnel_log_tail_emits_progress_notifications).
  • Integration test connects an MCP client over a stub WebSocket, asserts tools/list enumeration, and round-trips osinfo + process_list + file_read (mcp_tunnel_serves_initialize_list_and_tool_calls).
  • Zero mutating tools registered. registry_passes_read_only_check in host::mcp enumerates the live registry and trips if any denylisted name appears.

Test plan

  • cargo build --workspace
  • cargo test --workspace (260 lib + 7 handshake + 2 mcp_tunnel + 19 wsproxy + 24 tcpforward + 10 mcp + ...; all green)
  • cargo clippy --workspace --all-targets -- --deny warnings
  • cargo fmt --all --check
  • Manual MCP client round-trip against a live agent (lands once the VS-side gateway in VS-22 exposes the HTTP endpoint).
## Summary - Add `USAGE_MCP=8` tunnel mode (`crates/meshagent/src/host/mcp.rs`) that exposes the agent's read-only host inventory to MCP (Model Context Protocol) clients reached through the rendezvous tunnel. - New `meshagent-mcp` crate carries a transport-agnostic JSON-RPC 2.0 server (handshake, `tools/list`, `tools/call` + `notifications/progress`, `ping`) speaking the current MCP spec revision (`2025-11-25`). - v1 mutating-tool gate: `DENYLISTED_TOOL_NAMES` + `verify_read_only` refuse to serve a registry that carries any destructive tool (`exec`, `file_write`, `power_reboot`, ...). Lifted in v2 (VA-27) behind a `mutating-tools` feature + the consent layer. - 17 read-only tools exposed: `osinfo`, `cpuinfo`, `sysinfo`, `disks`, `users`, `process_list`, `dir_list`, `file_read`, `file_stat`, `services_list`, `packages_list`, `listen_ports`, `net_interfaces`, `dns_lookup`, `tcp_probe`, `time_status`, `log_tail` (streaming). - `log_tail` demonstrates `notifications/progress`: one progress event per line addressed at the caller's `_meta.progressToken`, with the full result still returned so non-subscribing clients see complete output. ## Deviations from the issue body - The VA-26 proposed-approach line called for `rmcp` (Anthropic's official Rust MCP SDK). I deferred that to v2. None of rmcp 1.7's built-in transports (stdio, Streamable HTTP) line up with the channel-backed tunnel transport without a duplex adapter, and the hand-rolled JSON-RPC is ~300 lines that we can swap for rmcp once the v2 consent layer forces typed schemas. Captured in the file-level doc on `meshagent-mcp/src/lib.rs`. ## Acceptance criteria status - New `meshagent-mcp` crate compiles cleanly; `cargo clippy --workspace --all-targets -- --deny warnings` passes. - `TunnelMode::Mcp` (`USAGE_MCP = 8`) routed in `host/tunnel.rs`. - 17 read-only host modules exposed as MCP tools, each with a JSON Schema input descriptor. - `log_tail` emits MCP progress notifications and terminates cleanly on tunnel close (verified by `mcp_tunnel_log_tail_emits_progress_notifications`). - Integration test connects an MCP client over a stub WebSocket, asserts `tools/list` enumeration, and round-trips `osinfo` + `process_list` + `file_read` (`mcp_tunnel_serves_initialize_list_and_tool_calls`). - Zero mutating tools registered. `registry_passes_read_only_check` in `host::mcp` enumerates the live registry and trips if any denylisted name appears. ## Test plan - [x] `cargo build --workspace` - [x] `cargo test --workspace` (260 lib + 7 handshake + 2 mcp_tunnel + 19 wsproxy + 24 tcpforward + 10 mcp + ...; all green) - [x] `cargo clippy --workspace --all-targets -- --deny warnings` - [x] `cargo fmt --all --check` - [ ] Manual MCP client round-trip against a live agent (lands once the VS-side gateway in VS-22 exposes the HTTP endpoint).
feat(tunnel): MCP read-only server tunnel mode (VA-26)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 4m12s
bcb619a1b7
Add USAGE_MCP=8 tunnel mode that exposes the agent's read-only host inventory to MCP (Model Context Protocol) clients reached through the rendezvous tunnel. The agent runs an in-process JSON-RPC 2.0 server speaking the subset of MCP needed for v1: `initialize` + `notifications/initialized`, `tools/list`, `tools/call` (with `notifications/progress` for streaming tools), and `ping`. Wire framing is one JSON-RPC message per WebSocket Text frame; binary frames are dropped.

The protocol implementation lives in a new `meshagent-mcp` crate that is transport-agnostic: it consumes a pair of `mpsc::Sender<String>` / `mpsc::Receiver<String>` channels so unit tests drive the server directly without spinning up a WebSocket. `host::mcp` adapts the relay WS to that channel pair and owns the tool registry that calls into the existing read-only host modules (osinfo, cpuinfo, sysinfo, disks, users, process_list, dir_list, file_read, file_stat, services_list, packages_list, listen_ports, net_interfaces, dns_lookup, tcp_probe, time_status, log_tail).

v1 mutating-tool gate: `meshagent_mcp::DENYLISTED_TOOL_NAMES` enumerates names that must NOT be registered while the read-only-only gate is in force (`exec`, `file_write`, `services_start`, `power_reboot`, `process_kill`, etc.). `verify_read_only` turns the denylist into a runtime check the agent runs at tunnel-open; a denylisted name refuses to serve rather than silently exposing mutating capability. The same check runs as a unit test against the live v1 registry. v2 (VA-27) will lift this gate behind a `mutating-tools` feature plus the consent layer.

Streaming: `log_tail` emits one `notifications/progress` per line as the journalctl / /var/log/* tail builds, addressed at the caller-supplied `_meta.progressToken`. The full result still contains every line so non-subscribing clients see the complete output. Tools with no progress subscriber take the no-op sender path and skip the channel + drain task entirely.

`rmcp` (the official Anthropic Rust MCP SDK) is intentionally NOT used in v1. The tunnel transport story (channel-backed, not stdio or HTTP) does not fit any of rmcp's built-in transports cleanly without a duplex adapter, and the hand-rolled framing is ~300 lines that can be swapped for rmcp in v2 once the consent layer forces typed schemas. The VA-26 issue body proposed rmcp; this deviation is captured here and on the issue comment.

Tests: 10 new unit tests in `meshagent-mcp` cover the JSON-RPC framing (initialize, tools/list ordering, tools/call dispatch, progress notification ordering, denylist enforcement, error -> isError result mapping, notification-no-reply contract); 3 new unit tests in `host::mcp` cover the v1 tool inventory and annotation invariants; 2 new integration tests in `tests/mcp_tunnel.rs` drive the full WebSocket path with the live `host::mcp::run` entry point and exercise the headline tools the AC names (`osinfo`, `process_list`, `file_read`) plus the `log_tail` streaming path.

#VA-26 State Done

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
David merged commit d8f4c9607a into main 2026-05-20 10:59:41 +02:00
David deleted branch feat/mcp-readonly-va-26 2026-05-20 10:59:41 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/vervain-agent!22
No description provided.