Skip to content

Architecture revamp: Go control plane + AI-native CLI + MCP server#3

Merged
sraodev merged 6 commits into
masterfrom
architecture-revamp
Apr 30, 2026
Merged

Architecture revamp: Go control plane + AI-native CLI + MCP server#3
sraodev merged 6 commits into
masterfrom
architecture-revamp

Conversation

@sraodev

@sraodev sraodev commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Summary

Reshapes the project around one long-lived daemon that owns the radio
and a versioned wire protocol that anything else can speak to it. The
typed CLI, the AI planner, the MCP server, the plan record/replay tool,
and (future) language SDKs and microservices are all just adapters on
the same surface.

The original PyBluez RFCOMM client/server is preserved at sdk/python/
as the reference implementation; nothing in the new stack depends on it
at runtime.

                  Inbound adapters
   ┌─────────┬─────────┬─────────┬───────────────┬─────────────┐
   │ ubtctl  │ ubtctl  │ ubtctl  │  ubtctl plan  │ MCP clients │
   │ (typed) │  ask    │  mcp    │  show / run   │ Editors/IDE │
   └────┬────┴────┬────┴────┬────┴───────┬───────┴──────┬──────┘
        │         │         │            │              │
        └─────────┴────┬────┴────────────┴──────────────┘
                       │   v1 wire protocol (length-prefixed JSON / UDS)
                       ▼
              ┌──────────────────┐
              │      ubtd        │   Go daemon: dispatcher, session manager,
              │   control plane  │   structured logs, audit, signal shutdown
              └────────┬─────────┘
                       │   transport.Driver
                       ▼
   ┌──────────┬──────────────┬────────────────┬──────────────────┐
   │   stub   │ linuxrfcomm  │ corebluetooth  │      winrt       │
   │ (any OS) │  (Linux)     │  (TODO)        │  (TODO)          │
   └──────────┴──────────────┴────────────────┴──────────────────┘

What's in this PR (seven self-contained commits)

# Commit Highlights
1 Go daemon + CLI scaffold (P0–P2) Wire protocol IDL + framing spec; sdk/go/pkg/{protocol,sockaddr,transport}; ubtd UDS server; ubtctl typed verbs (ping, version, status, capabilities, discover, send); codec round-trip tests.
2 AI planner — ubtctl ask Claude Opus 4.7 with adaptive thinking and prompt-caching on the system prompt + tool list; five tools auto-derived from the daemon RPC surface; --dry-run/--yes safety modes for the only mutating tool.
3 linuxrfcomm driver Real RFCOMM via AF_BLUETOOTH / SOCK_STREAM / BTPROTO_RFCOMM socket; Discover shells out to bluetoothctl devices; build-tagged _other.go keeps cross-OS builds clean. Daemon dispatcher now preserves typed protocol.Error codes from drivers (errors.As) instead of flattening every failure to transport_error.
4 MCP server — ubtctl mcp JSON-RPC 2.0 over newline-delimited stdio; initialize / ping / tools/list / tools/call; protocol revision 2025-03-26. Refactored the tool surface into a neutral cli/ubtctl/tools package so the AI planner and the MCP server share one source of truth.
5 Plan record / replay ubtctl ask --save plan.json captures the tool-call trace; ubtctl plan show / run replay it later — no LLM, no spend. Mutating steps gated behind --yes; pre-flight check fails fast instead of running half a plan. format_version is validated.
6 PR #1 fixes adopted into the modern Python SDK The remaining real fixes from the long-open nrathaus PR (#1) — connection retry with backoff, install pybluez from GitHub source — adapted to the refactored sdk/python/ shape. Binary 2-byte length framing and sys.exit-on-failure intentionally not adopted (regressions vs the modern code).
7 README sweep Top-level README rewritten as the canonical architecture document (diagram, pros/cons table, repository layout, three quick-start paths, full usage walkthrough, roadmap). Twelve more READMEs (per-package + per-example + per-microservice) re-anchored in the new architecture.

What's new for users

  • ubtd — daemon with --driver {stub|linuxrfcomm}, --socket, --log-json, signal-driven shutdown.
  • ubtctl — single binary, ten subcommands. Typed verbs unchanged in shape from a Cobra-style CLI; ask / mcp / plan are new.
  • MCP integration — drop ubtctl mcp into any MCP-aware editor/agent config; tools auto-populate.
  • Plan replay — captured AI runs become version-controllable, reviewable, replayable scripts. Read-only plans run unattended; mutating plans require explicit --yes.

Architecture promises this PR is designed to keep

  • Adding a new transport driver = implement one Go interface (transport.Driver). Every layer above it works unchanged.
  • Adding a new way to drive the daemon = another consumer of the wire protocol. The hex doesn't change.
  • Adding a new RPC = one declaration in cli/ubtctl/tools mechanically extends the typed CLI, the AI planner's tool registry, and the MCP server's tools/list. They cannot drift.
  • AI runs are auditable as plain CLI commands because tools are 1:1 with daemon RPCs and runs can be captured to JSON for replay.

What is preserved

  • The full Python RFCOMM SDK at sdk/python/ — production-ready, tests
    covering protocol behaviour. Reframed as the reference SDK rather
    than the production surface.
  • The original wire format ideas captured in common/protocol/.
  • All public interfaces in sdk/python/bluetooth_service/ (the PR Multiple fixes to bring the code to work with latest version of Python3 #1
    fixes are additive — no existing field was removed).

Roadmap (deferred from this PR)

  • Listen / Reply / CloseSession RPCs + transport.Listener for
    bidirectional sessions (foundation for the chat example).
  • CoreBluetooth (macOS) and WinRT (Windows) drivers.
  • gRPC v2 wire generated from common/protocol/v1.proto.
  • microservices/{grpc,rest}-server as remote facades.

Test plan

  • go build ./... clean on linux/amd64; cross-compiles to darwin and windows
  • go vet ./... clean
  • go test ./... — codec round-trip and linuxrfcomm parsers pass
  • End-to-end smoke: ubtd --driver stub + every ubtctl subcommand against it
  • MCP server smoke: piped initialize / tools/list / tools/call for all five tools, plus an unknown-tool path returning -32601
  • Plan replay smoke: show / run --dry-run / run (refused) / run --yes / version-mismatch path
  • PR Multiple fixes to bring the code to work with latest version of Python3 #1 retry path verified with a stubbed bluetooth module (3 attempts, configurable backoff, last error preserved as BluetoothServerError.cause)
  • Live-hardware smoke for linuxrfcomm (no Bluetooth radio available in the dev environment used to ship this PR)

https://claude.ai/code/session_01McVy2bpGps4r5RWbBgjinf


Generated by Claude Code

claude added 6 commits April 30, 2026 19:44
Phase 4 of the architecture plan. The planner is a thin shell around the
existing daemon RPC surface — every Claude tool wraps a single ubtd
method, so AI-driven runs and typed CLI commands share one execution
path. This is what makes AI runs auditable as plain CLI commands.

- cli/ubtctl/ai: tool registry, system prompt, planner.
  - BuildTools wraps each daemon RPC as an SDK BetaTool with a typed
    input struct and a jsonschema-tagged schema. ping_daemon, get_status,
    get_capabilities, discover_devices (streams from the daemon, hands
    Claude a single aggregated list), send_payload (gated by ExecMode).
  - Planner uses BetaToolRunnerStreaming with adaptive thinking, an
    8-iteration ceiling, and a stable system prompt + tool list marked
    with cache_control: ephemeral so subsequent runs read the cached
    prefix. Default model claude-opus-4-7.
- cli/ubtctl/commands/ask: subcommand plumbing. --dry-run stubs the
  mutating tool, --yes auto-approves it, --model overrides Claude.
  Signal-based cancellation (no fixed deadline).
- README documents the planner surface and required ANTHROPIC_API_KEY.
Phase 1 of the next-step trio (driver / MCP / plan replay). The daemon
no longer has only a stub — on Linux, ubtd can talk to actual hardware.

- sdk/go/pkg/transport/linuxrfcomm: new driver, build-tagged.
  - driver_linux.go: Send opens AF_BLUETOOTH/SOCK_STREAM/BTPROTO_RFCOMM
    via the kernel directly (x/sys/unix.SockaddrRFCOMM), reverses
    the bdaddr to wire order, honours ctx deadlines via SO_SNDTIMEO.
    Discover enumerates BlueZ-known peers via `bluetoothctl devices`
    — not a live scan, but zero D-Bus deps in the daemon for v1.
  - driver_other.go: !linux placeholder that registers cleanly,
    advertises Discover=false / Stream=false, and returns
    not_implemented from Send/Discover. Keeps the daemon portable.
  - tests cover bdaddr parsing (both ends + invalid) and the
    bluetoothctl line parser (Device prefix, malformed, controller).
- ubtd: --driver {stub|linuxrfcomm} flag replaces --stub. Default
  stays "stub" for portability. --bluetoothctl path override for
  unprivileged tests.
- Daemon dispatcher: writeDriverError preserves the typed
  protocol.Error code from drivers (errors.As) instead of always
  flattening to transport_error. Send with a malformed address now
  surfaces invalid_params, which the AI planner can react to.
- Cross-compiles clean on darwin/windows; tests pass on linux.
Phase 2 of the trio. One tool registry, two presentations: the in-process
AI planner (ubtctl ask) and the MCP server (ubtctl mcp) now share a
single source of truth in cli/ubtctl/tools. Adding a daemon RPC
mechanically extends both surfaces.

- cli/ubtctl/tools: neutral Spec + Result + Registry. Generic New[T]
  derives the JSON Schema from struct tags via invopop/jsonschema.
- cli/ubtctl/ai/tools: BuildSpecs builds the registry; AsBetaTools
  adapts each Spec into an anthropic.BetaTool for the SDK runner.
  Existing planner code now consumes the registry instead of building
  BetaTools directly.
- cli/ubtctl/mcp: JSON-RPC 2.0 over newline-delimited stdio. Implements
  initialize, ping, tools/list, tools/call. Protocol revision
  2025-03-26. Logs go to stderr only — stdout is reserved for the
  protocol stream.
- cli/ubtctl/commands/mcp: 'ubtctl mcp' subcommand with --socket flag,
  signal-based shutdown, structured slog.
- Smoke-tested all five tools end-to-end against the stub driver via
  piped JSON-RPC. Unknown-tool path returns -32601 cleanly.
- Schema-tag fix: invopop/jsonschema treats commas in jsonschema:
  "description=..." as tag separators, silently truncating descriptions.
  Reworded all descriptions to use semicolons / slashes instead.
Phase 3 of the trio. ubtctl ask can now capture its tool-call trace to
a JSON file; ubtctl plan show/run replay it later without going back
to the LLM. Mutating steps are gated behind --yes — read-only plans
run unattended, side-effecting plans require explicit confirmation.

- cli/ubtctl/tools: Spec gains a Mutating flag. NewMutating[T] wraps
  the constructor; send_payload is the only tool currently flagged.
- cli/ubtctl/ai/plan.go: Plan + Step JSON shape (format_version: 1),
  recording wrapper that decorates Spec.Handler to append a Step on
  every call, SavePlan / LoadPlan / Replay with pre-flight mutating-
  step gate that fails fast instead of running half a plan.
- cli/ubtctl/ai/planner.go: ai.Plan renamed to ai.RunConfig (avoids
  collision with the new on-disk Plan); SavePath wires recording in
  via wrapWithRecorder when set.
- cli/ubtctl/commands/ask.go: --save FILE flag.
- cli/ubtctl/commands/plan.go: 'ubtctl plan show' (pretty-print),
  'ubtctl plan run' (replay) with --socket / --dry-run / --yes.
- Smoke-tested all four paths: show formats; --dry-run prints without
  contacting ubtd; gate refuses mutating plans without --yes; --yes
  runs the captured plan against the stub driver and matches the
  recorded result. format_version mismatch is rejected cleanly.
- README sweep: top-level README rewritten to reflect everything that
  has actually shipped (Go daemon, typed CLI, AI planner, MCP server,
  plan replay, linuxrfcomm driver) instead of "future Go SDK"
  placeholders. cli/ubtctl/README.md gains plan + MCP sections.
PR #1 was opened against the original root-level scripts in 2023 and
no longer applies cleanly to the refactored sdk/python/ tree, but two
of its fixes are still real and weren't ported during the refactor.

- ClientSettings: connect_retries (default 5) and connect_backoff_seconds
  (default 1.0). Mirrors the discovery_retries / discovery_backoff_seconds
  pattern already in place.
- ClientSocketManager.connect(): wrap the connect call in a retry loop
  with the configured backoff. Bluetooth connect() is genuinely flaky
  in practice; the original PR did this with a hardcoded attempt > 5
  guard, here it is config-driven and logs per attempt. Last error is
  preserved as the raised BluetoothServerError's cause.
- install_dependencies.sh + sdk/python/README: install pybluez from
  github.com/pybluez/pybluez instead of PyPI. The PyPI distribution
  hasn't been updated in years and fails to build on Python 3.10+.

Not adopted from PR #1:
- Binary 2-byte little-endian length framing — caps payloads at 64KB
  and would regress the current decimal-string framing on both sides.
- bare-Exception catches and sys.exit(0) on failure — the current
  module raises typed BluetoothClientError / BluetoothServerError,
  which is strictly better for callers and tests.

Smoke-tested the retry path with a stubbed bluetooth module (3 attempts
with 0.01s backoff completed in ~20ms; each attempt logged; final
exception carried the BluetoothError cause).
Every README in the repo (top-level + per-package + per-example) now
opens with how that piece fits the hexagonal control-plane design,
not the legacy PyBluez story.

Top-level README.md:
  - Architecture diagram with inbound adapters, the wire protocol +
    daemon at the centre, and outbound transport drivers.
  - "Why this shape" table — pros/cons of every major decision (Go
    daemon vs Python, JSON-over-UDS vs gRPC, AI planner, MCP server,
    tool-registry single-source-of-truth, mutating-step plan gate).
  - Repository layout where every directory maps to one architectural
    layer.
  - Three quick-start paths: pure Go (stub driver), Linux with real
    hardware (linuxrfcomm), and the Python reference SDK.
  - Full usage walkthrough for every entry point: typed CLI, AI
    planner (with --save), MCP server, plan record/replay, and the
    ubtd flag surface.
  - Wire-protocol section pointing at v1.proto + framing.md + the Go
    bindings.
  - Roadmap with phase numbers tied to the example READMEs that
    consume each phase.

Per-package READMEs replaced or rewritten to fit the new architecture:
  - common/protocol: how to add a method (the 6-step lockstep flow).
  - common/message-schema: payload shapes vs wire protocol; status
    table per planned schema.
  - sdk/go: package responsibilities table; full driver-contract
    code; status table per component.
  - sdk/python: re-anchored as the reference SDK, not the production
    surface.
  - sdk/rust: planned workspace shape; client-vs-driver roles.
  - cli/ubtctl: sub-package responsibility table; the "one-rpc-extends-
    every-front-end" architectural promise spelled out.
  - microservices/{grpc,rest}-server: each is "another inbound
    adapter", not a re-implementation. REST README documents three
    implementation paths and explicit ownership of auth/policy.
  - examples/chat, file-transfer, sensor-stream: each names the
    upstream roadmap phase it depends on, the anticipated CLI shape,
    and where its payload schema will land in common/message-schema.
  - docs/: index pointing at canonical per-package docs; what lands
    here over time.

No code changes. Build + tests green.
@sraodev
sraodev merged commit d163e87 into master Apr 30, 2026
@sraodev
sraodev deleted the architecture-revamp branch April 30, 2026 20:42
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.

2 participants