Architecture revamp: Go control plane + AI-native CLI + MCP server#3
Merged
Conversation
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.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
What's in this PR (seven self-contained commits)
sdk/go/pkg/{protocol,sockaddr,transport};ubtdUDS server;ubtctltyped verbs (ping,version,status,capabilities,discover,send); codec round-trip tests.ubtctl ask--dry-run/--yessafety modes for the only mutating tool.linuxrfcommdriverAF_BLUETOOTH / SOCK_STREAM / BTPROTO_RFCOMMsocket;Discovershells out tobluetoothctl devices; build-tagged_other.gokeeps cross-OS builds clean. Daemon dispatcher now preserves typedprotocol.Errorcodes from drivers (errors.As) instead of flattening every failure totransport_error.ubtctl mcpinitialize/ping/tools/list/tools/call; protocol revision2025-03-26. Refactored the tool surface into a neutralcli/ubtctl/toolspackage so the AI planner and the MCP server share one source of truth.ubtctl ask --save plan.jsoncaptures the tool-call trace;ubtctl plan show / runreplay it later — no LLM, no spend. Mutating steps gated behind--yes; pre-flight check fails fast instead of running half a plan.format_versionis validated.nrathausPR (#1) — connection retry with backoff, install pybluez from GitHub source — adapted to the refactoredsdk/python/shape. Binary 2-byte length framing andsys.exit-on-failure intentionally not adopted (regressions vs the modern code).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/planare new.ubtctl mcpinto any MCP-aware editor/agent config; tools auto-populate.--yes.Architecture promises this PR is designed to keep
transport.Driver). Every layer above it works unchanged.cli/ubtctl/toolsmechanically extends the typed CLI, the AI planner's tool registry, and the MCP server'stools/list. They cannot drift.What is preserved
sdk/python/— production-ready, testscovering protocol behaviour. Reframed as the reference SDK rather
than the production surface.
common/protocol/.sdk/python/bluetooth_service/(the PR Multiple fixes to bring the code to work with latest version of Python3 #1fixes are additive — no existing field was removed).
Roadmap (deferred from this PR)
Listen/Reply/CloseSessionRPCs +transport.Listenerforbidirectional sessions (foundation for the chat example).
common/protocol/v1.proto.microservices/{grpc,rest}-serveras remote facades.Test plan
go build ./...clean on linux/amd64; cross-compiles to darwin and windowsgo vet ./...cleango test ./...— codec round-trip andlinuxrfcommparsers passubtd --driver stub+ everyubtctlsubcommand against itinitialize/tools/list/tools/callfor all five tools, plus an unknown-tool path returning-32601show/run --dry-run/run(refused) /run --yes/ version-mismatch pathbluetoothmodule (3 attempts, configurable backoff, last error preserved asBluetoothServerError.cause)linuxrfcomm(no Bluetooth radio available in the dev environment used to ship this PR)https://claude.ai/code/session_01McVy2bpGps4r5RWbBgjinf
Generated by Claude Code