Write #[command] fn, get a discoverable RPC server — anywhere a byte-stream goes.
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.
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 (tools/telepath) auto-generates MCP tool descriptors from live
#[command] metadata — zero hand-written tool definitions required:
client.discover()fetches all commands; eachSchemaEntryholdsargs_schema/ret_schemaas postcard-encodedOwnedNamedTypebytes.SchemaEntry::decoded_args_schema()/decoded_ret_schema()decode those bytes intopostcard_schema::schema::owned::OwnedNamedType.schema_to_json::named_type_to_json_schema()mapsOwnedNamedTypeto a JSON Schema value suitable as the MCPinputSchema.bridge::invoke()converts MCP tool call arguments (JSON) to postcard, callsclient.call_raw(), and converts the response back to JSON.
// discover → MCP tool (actual API)
let _n = client.discover()?;
for entry in client.schema_cache().iter() {
let args_schema = entry.decoded_args_schema()?;
let json_schema = named_type_to_json_schema(&args_schema);
// rmcp::Tool auto-registered with json_schema as inputSchema
}
// MCP tool call → bridge::invoke → call_raw → JSON response
See docs/mcp-integration.md and
tools/telepath/README.md
for setup instructions, including using it from Claude Code.
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.
| 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.0 # 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 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.
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 pingAnnotate 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);# Format check
cargo fmt --all -- --check
# Clippy (warnings are errors)
cargo clippy --workspace -- -D warnings
# All checks at once
just ciSee 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:
publish = falseeverywhere until the API stabilizes
Releases are driven by Conventional Commits on main — no manual tagging required.
For the full developer flow (including the required just bump-excluded step on the
release PR), 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).
All Renovate PRs require human review; auto-merge is disabled.
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.