Embedded RPC for Rust: write #[command] on the target and get a discoverable RPC server callable from a host shell or AI agent over any byte-stream transport.
Telepath lets you expose any MCU-side logic over the wire so you can call it interactively from a host shell or an AI agent — giving you real hardware feel while validating behaviour and API ergonomics without instrumenting tests.
It splits the problem in two:
- Server (target): command definitions and a poll loop. No shell, no scheduler integration, no allocator.
- Client (host):
telepath shell(the unified CLI) for interactive use;telepath-clientlib for building your own host or MCP server frontend.
Three things make it unusual:
- One attribute, zero boilerplate.
#[command]generates the wire shim, schema metadata, and link-time registration. No IDL, no manual sync. - Schemas travel on the wire. The host discovers commands at runtime; firmware changes never silently break the client.
- Transport is a two-method trait. No RTOS lock-in. UART, RTT, USB-CDC, BLE, mpsc — all valid backends.
Minimal example:
#[command]
fn ping() -> u32 { 0xDEAD_BEEF }That one attribute registers ping in the command table, generates its wire shim, and embeds its postcard schema — no further wiring needed.
Telepath's wire design follows two established protocol models rather than inventing new ones: packet framing follows the ONC RPC (RFC 5531) CALL/REPLY split, and its discovery command ID reuses the CoAP (RFC 7252) Empty/NULL convention. Schema-on-the-wire discovery exists specifically so a host — human or AI agent — never needs a hand-written or out-of-band description of what a target firmware build can do.
sequenceDiagram
participant H as Host (telepath-client)
participant T as Target Server (telepath-server)
Note over T: Link time: #[command] collects CommandMetadata
H->>T: Connect (transport open)
H->>T: Request [CmdID: 0x0000] — Discovery
T-->>H: Response — command list {id, name}
Note over H: Build dynamic client map
H->>T: Request [CmdID: N] — postcard-encoded args
T->>T: Deserialize args → dispatch → serialize result
T-->>H: Response [status, postcard-encoded payload]
H->>H: Decode payload → present to caller
Telepath is a five-crate workspace (telepath-wire, telepath-macros,
telepath-server, telepath-client, examples/host-pty-server) plus two
workspace-excluded crates (tools/telepath, examples/nrf52840-ping).
See AGENTS.md § Workspace Overview for the
full table with target triples and Cargo feature flags.
| Direction | Method | Rationale |
|---|---|---|
| Host → Target | COBS | Minimal decoder on MCU: read_until(0x00) |
| Target → Host | rzCOBS | Removes trailing-zero overhead; improves throughput for sparse sensor data |
Both directions use 0x00 as the frame delimiter.
Two packet types only (Request / Response), following the ONC RPC RFC 5531
CALL/REPLY model. Errors live in ResponseStatus, not as separate packet types.
CmdID 0x0000 is reserved for the Command Discovery Protocol (CDP).
Telepath's wire protocol is designed so that a host can enumerate commands and their full type signatures at runtime — the foundation needed to drive a Telepath server from an AI agent without hand-written tool descriptors.
DiscoveryEntry.args_schema/ret_schemacarry realpostcard-schemabytes (NamedTypeserialised with postcard) over the wire.client.discover()fetches all commands via CDP paging; the result lands inSchemaCache, keyed by command ID.examples/host-pty-serverexercises this end-to-end over a real PTY transport — no hardware required.
The telepath mcp subcommand auto-generates MCP tool descriptors from live
#[command] metadata — zero hand-written tool definitions required. It
discovers every command and its postcard schema at runtime, maps each schema
to a JSON Schema for the tool's input, and bridges MCP tool calls to RPC calls
by converting JSON arguments to postcard and the response back to JSON.
See the MCP integration guide for the pipeline, the
schema mapping, and running it, and the
telepath CLI README for the subcommand's usage.
| Tool | Purpose |
|---|---|
| Rust stable | Build host workspace |
rustup target add thumbv7em-none-eabi |
Firmware cross-compilation |
probe-rs |
Flash and run firmware on nRF52840-DK |
just |
Task runner (optional but recommended) |
cocogitto |
Conventional Commits enforcement (commit-msg hook) |
MSRV: See Supported Rust Version below for the declared Minimum Supported Rust Version and policy.
The unified CLI ships three install paths:
| Method | Command | Notes |
|---|---|---|
| Source build | cargo install telepath |
Compiles from crates.io. Minutes; no extra tooling. |
| Pre-built binary | cargo binstall telepath |
Fetches the matching archive from GitHub Releases. Requires cargo-binstall. |
| Manual download | See below | No Rust required. |
Manual download — choose your target triple and run:
VERSION=0.2.1 # replace with desired release
TARGET=x86_64-unknown-linux-gnu # or: aarch64-unknown-linux-gnu, aarch64-apple-darwin
curl -L "https://github.com/tarotene/telepath/releases/download/v${VERSION}/telepath-${TARGET}.tar.gz" \
| tar -xzf -
./telepath-${TARGET}/telepath --helpWindows: download telepath-x86_64-pc-windows-msvc.zip from the
Releases page and extract.
macOS Intel (x86_64-apple-darwin) users: pre-built binaries are not available yet — use
cargo install telepath instead.
Pre-built binaries ship with default features (shell + mcp + rtt). The serial
transport requires libudev on Linux and is available only via source build:
cargo install telepath --features serial.
The fastest way to see Telepath in action requires no hardware.
git clone https://github.com/tarotene/telepath.git
cd telepath
just host-pty-smokeExpected output:
ping -> 0xDEADBEEF
host-pty-smoke starts examples/host-pty-server (a TelepathServer over a PTY
master), then drives it from telepath shell --transport serial via the slave end.
The full wire path (postcard serialization + COBS framing) runs identically to real
hardware. Switching to an MCU is purely a transport swap.
Annotate functions with #[command] and pass them to TelepathServer::new in a poll() loop:
use telepath_server::{command, TelepathServer};
#[command]
fn ping() -> u32 { 0xDEAD_BEEF }
let mut server = TelepathServer::<MyTransport, 512>::new(transport, telepath_server::commands());
loop { server.poll(); }See telepath-server § Usage for the full recipe including
#[resource] injection and the postcard direct-dependency requirement.
[dependencies]
telepath-client = { git = "https://github.com/tarotene/telepath", branch = "main", features = ["rtt"] }use telepath_client::TelepathClient;
// transport: anything implementing `std::io::Read + std::io::Write`
let mut client = TelepathClient::new(transport);
// Discover commands registered on the target, then issue a typed call.
client.discover()?;
let ping_id = client.cmd_id_by_name("ping").expect("ping not registered");
let val: u32 = client.call::<(), u32>(ping_id, &())?;
println!("ping -> 0x{:08X}", val);See examples/nrf52840-ping/README.md for the
full hardware walk-through (udev rules, APPROTECT unlock, RTT channel layout).
# Flash firmware (downloads and exits; probe is released)
cd examples/nrf52840-ping && cargo run --release
# Ping over RTT (RPC traffic on channel 1)
cd tools/telepath && cargo run -- shell --exec pingIn:
- The
#[command]macro and the server/client/wire crates - The unified
telepathCLI (shellREPL +mcpserver, auto-generated tool descriptors) - Transport backends (UART/RTT/USB-CDC/BLE/PTY)
- On-wire schema discovery (CDP)
Out:
- Board support and specific firmware application logic beyond reference examples
- RTOS/scheduler integration
- Repository governance templates (the responsibility of a separate Rust repository governance skill, such as the one used by
dotfiles)
The repository ships hooks under .githooks/ that enforce quality gates at
commit and push time. They are not active by default — Git reads hooks from
.git/hooks/ unless told otherwise.
Run once per clone to wire them up:
git config --local core.hooksPath .githooksThe three hooks (commit-msg, pre-commit, pre-push) and their expected
wall times are documented in AGENTS.md § Git Hooks.
Why this split? Commits happen frequently, so pre-commit runs only the
instant format check. Pushes are less frequent and signal intent to share code,
so pre-push runs the slower static analysis and test suite. The full CI gate
(just ci) additionally runs the PTY-based host-pty-server smoke (host-pty-smoke) and the telepath CLI tests
(mcp-test), and is intentionally left to CI — see CI / Quality gates. commit-msg fires before pre-commit and validates only the message format via cocogitto — instant feedback with no build step.
Hook does not run. Check git config core.hooksPath. If it prints a path
other than .githooks (e.g. ~/.config/git/hooks set globally), the
repo-local setting above overrides it once you run the command.
just: command not found. Install via cargo install just or your OS
package manager.
cog: command not found. Install cocogitto via cargo install --locked cocogitto.
Bypass in an emergency. Pass --no-verify to skip hooks:
git commit --no-verify. The CI gate still applies on every PR.
The full list of build and test commands — host workspace, firmware example,
the workspace-excluded tools/telepath CLI, format/clippy gates, and the
just ci aggregator — lives in
AGENTS.md § Build Commands.
For first-time setup, run through Quickstart above first.
Run just ci locally before opening a PR — it covers format check, clippy, tests, the PTY smoke, and the CLI tests.
Full gate details and individual commands live in AGENTS.md § Build Commands and AGENTS.md § Required CI gates.
See CHANGELOG.md for the project history.
Telepath uses unified versioning: all workspace members share the same vX.Y.Z
version and are released together as a single GitHub Release tagged vX.Y.Z.
- CHANGELOG:
CHANGELOG.md— updated automatically by release-plz - GitHub Releases: Releases page
- crates.io:
telepath-wire,telepath-macros,telepath-server,telepath-client,telepath— published since v0.2.0; example crates are not published
Releases are driven by Conventional Commits on main — no manual tagging required.
For the full developer flow see AGENTS.md § How releases work.
Telepath declares a Minimum Supported Rust Version (MSRV) of 1.88.
This applies to all workspace members and the excluded crates
(tools/telepath, examples/nrf52840-ping).
The MSRV is verified in CI via the msrv job (dtolnay/rust-toolchain@1.88.0).
A bump to the MSRV is treated as a MINOR change under SemVer for pre-1.0
releases. See AGENTS.md § Toolchain for the
full MSRV policy including manifest updates and commit convention.
Renovate opens dependency-bump PRs every Monday morning (JST).
Per the account-wide policy preset that renovate.json extends, non-major updates
(except 0.x minor) auto-merge once the required checks pass; major and 0.x minor
updates stay human-reviewed PRs.
Embedded HAL updates (embassy-*, nrf-pac, cortex-m-rt, …) carry the
needs-smoke-test label and require an on-device just firmware-ping run on
nRF52840-DK before merging.
Licensed under either of
at your option.
The banner illustration (docs/assets/readme-banner.png)
is by Irasutoya
(source page) and is
not covered by this repository's license; it is used under the
Irasutoya terms of use.
