ToolAPI aims to connect tools and clients written in any language. It should be possible to call a Rust tool from a Python optimization script, or a Python tool from a JavaScript web app, without worrying about language-specific details.
The Rust crate is the single source of truth — it defines all value types, the wire protocol, and both server and client logic. The Python and JavaScript/WASM packages wrap this core with language-idiomatic APIs.
| Implementation | Package | Role |
|---|---|---|
| Rust | toolapi on crates.io | Core: defines types, protocol, server + client |
| Python | toolapi on PyPI | Client with idiomatic Python value classes |
| JavaScript / WASM | toolapi-wasm on npm | Client-only, async call() for web apps |
Rust
The canonical ToolAPI implementation. All other implementations depend on this crate. It provides:
- The complete Value type system
- MessagePack + zstd serialization (pure Rust, WASM-compatible)
- A WebSocket server framework for writing tools (
run_server) - A WebSocket client for invoking tools (
call)
Feature Flags
| Feature | Description |
|---|---|
server | Axum-based WebSocket server (native only) |
client | WebSocket client (tungstenite on native, ws_stream_wasm on wasm32) |
pyo3 | PyO3 FromPyObject / IntoPyObject impls for all Value types |
Both server and client are enabled by default.
Installation
[dependencies]
toolapi = "0.4"For client-only usage (e.g. in a CLI or script):
[dependencies]
toolapi = { version = "0.4", default-features = false, features = ["client"] }Writing a Tool (Server)
A tool is a function with the signature fn(Value, &mut MessageFn) -> Result<Value, ToolError>. It receives the client’s input as a Value, can send progress messages via the MessageFn, and returns a result.
use toolapi::{run_server, Value, MessageFn, ToolError};
fn my_tool(input: Value, send_msg: &mut MessageFn) -> Result<Value, ToolError> {
// Extract parameters from input (a Dict)
let iterations: i64 = input.get("iterations")?.try_into()?;
send_msg(format!("Running {iterations} iterations..."))?;
// ... perform computation ...
Ok(Value::Float(42.0))
}
fn main() -> Result<(), std::io::Error> {
run_server(my_tool, None)
}run_server starts an Axum WebSocket server on 0.0.0.0:8080:
GET /serves an optional static HTML page (passSome(INDEX_HTML)as second argument)/toolaccepts WebSocket connections from clients
Calling a Tool (Client)
use toolapi::{call, Value};
fn main() {
let input = Value::Dict(/* ... build input parameters ... */);
let result = call("wss://tool-example.fly.dev/tool", input, |msg| {
println!("[tool] {msg}");
true // return false to abort
});
match result {
Ok(output) => println!("Result: {output:?}"),
Err(err) => eprintln!("Error: {err}"),
}
}The on_message callback receives progress strings from the tool. Returning false sends an Abort signal.
Key Types
| Type | Description |
|---|---|
Value | Dynamic typed enum — the core data type exchanged between tool and client |
MessageFn | dyn FnMut(String) -> Result<(), AbortReason> — send progress, detect abort |
ToolFn | fn(Value, &mut MessageFn) -> Result<Value, ToolError> — tool signature |
ToolError | Error returned by a tool: Extraction, Abort, or Custom(String) |
ToolCallError | Client-side error from call(): connection, protocol, or tool errors |
Python
Python bindings wrapping the Rust toolapi crate via PyO3 and Maturin. Provides a native call() function and pure-Python dataclass wrappers for all Value types.
Installation
pip install toolapiNote
The package is named
toolapion PyPI (nottoolapi-py). It ships a compiled native extension for the platform — no Rust toolchain needed at install time.
Calling a Tool
from toolapi import call
def on_message(msg: str) -> bool:
print(f"[tool] {msg}")
return True # return False to abort
result = call(
"wss://tool-phantomlib-flyio.fly.dev/tool",
{
"fov": [0.3, 0.3, 0.3],
"resolution": [128, 128, 1],
},
on_message,
)The function signature is:
def call(
address: str,
input: Value,
on_message: Callable[[str], bool] | None = None,
) -> Valueaddress: WebSocket URL of the tool serverinput: Any Python object that maps to a ToolAPIValue(see below)on_message: Optional callback for progress messages; returnFalseto abort
The call blocks until the tool finishes. The GIL is released during the WebSocket communication, so other Python threads can run concurrently.
Value Type Mapping
Primitive Python types map directly to ToolAPI values:
| Python | ToolAPI Value |
|---|---|
None | None |
bool | Bool |
int | Int |
float | Float |
str | Str |
complex | Complex |
dict | Dict (heterogeneous) or TypedDict (homogeneous) |
list | List (heterogeneous) or TypedList (homogeneous) |
For homogeneous list and dict values, the Rust side automatically infers TypedList / TypedDict for efficient packing. Heterogeneous containers use List / Dict.
Structured Types
For MRI-specific structured types, the toolapi.value module provides Python dataclasses that mirror the Rust types:
from toolapi.value import Vec3, Vec4, Volume, PhantomTissue, SegmentedPhantom, InstantSeqEventVec3 / Vec4
v3 = Vec3([1.0, 2.0, 3.0])
v4 = Vec4([0.0, 0.0, 0.0, 1.0])Volume
A 3D voxel volume with an affine transform:
vol = Volume(
shape=[128, 128, 1],
affine=[
[0.002, 0.0, 0.0, -0.128],
[0.0, 0.002, 0.0, -0.128],
[0.0, 0.0, 0.002, 0.0],
],
data=[0.0] * (128 * 128), # TypedList inferred as Float
)PhantomTissue
A single tissue with density and off-resonance volumes, plus scalar relaxation parameters:
tissue = PhantomTissue(
density=density_volume,
db0=db0_volume,
t1=0.8,
t2=0.05,
t2dash=0.02,
adc=0.001,
)SegmentedPhantom
A multi-tissue phantom with B1 transmit/receive maps:
phantom = SegmentedPhantom(
tissues={"white_matter": wm_tissue, "gray_matter": gm_tissue},
b1_tx=[b1_tx_volume],
b1_rx=[b1_rx_volume],
)InstantSeqEvent
A tagged union constructed via factory methods:
pulse = InstantSeqEvent.Pulse(angle=3.14, phase=0.0)
fid = InstantSeqEvent.Fid(kt=[0.0, 0.0, 0.0, 0.01])
adc = InstantSeqEvent.Adc(phase=0.0)JavaScript / WASM
A client-only WASM wrapper around the Rust toolapi crate, compiled via wasm-pack and wasm-bindgen. Exposes a single async call() function for use in browser and other web-compatible environments.
WASM tool servers are not planned — tools should be hosted natively (in Rust or Python) and accessed from JavaScript as a client.
Installation
npm install toolapi-wasmOr build from source:
wasm-pack build --target webCalling a Tool
import init, { call } from "toolapi-wasm"
await init() // initialize WASM module
const input = {
Dict: {
resolution: { List: [{ Int: 128 }, { Int: 128 }, { Int: 1 }] },
flip_angle: { Float: 0.26 },
},
}
const result = await call("wss://tool-example.fly.dev/tool", input, (msg) => {
console.log(`[tool] ${msg}`)
return true // return false to abort
})The function signature is:
async function call(
addr: string,
input: Value,
on_message: (msg: string) => boolean,
): Promise<Value>- Returns a
Promisethat resolves to the result or rejects with anError - The
on_messagecallback is called for each progress message; returnfalseto abort - If the callback throws or returns a falsy value, the tool is aborted
Value Serialization
Values are serialized via serde_wasm_bindgen, using tagged objects where the key is the type name:
| ToolAPI type | JavaScript representation |
|---|---|
None | { "None": null } |
Bool | { "Bool": true } |
Int | { "Int": 42 } |
Float | { "Float": 3.14 } |
Str | { "Str": "hello" } |
Complex | { "Complex": { "re": 1.0, "im": 2.0 } } |
Vec3 | { "Vec3": [1.0, 2.0, 3.0] } |
Dict | { "Dict": { "key": <Value>, ... } } |
List | { "List": [<Value>, ...] } |
This tagged format matches Serde’s default enum serialization and is used for both input and output.