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 | Server + client with idiomatic Python classes |
| JavaScript / WASM | toolapi 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.5"For client-only usage (e.g. in a CLI or script):
[dependencies]
toolapi = { version = "0.5", 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 for invoking tools, run_server() for writing tools in Python, 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.
Writing a Tool (Server)
from toolapi import run_server
def my_tool(input, send_msg):
iterations = input["iterations"]
send_msg(f"Running {iterations} iterations...")
# ... perform computation ...
return {"result": 42.0}
run_server(my_tool)The function signature is:
def run_server(
tool: Callable[[Any, MessageFn], Any],
index_html: str | None = None,
) -> Nonetool: A callable(input, send_msg) -> result. Called for each client connection.send_msgsends a message string to the client and raises if the client requested abort.index_html: Optional HTML string served at the/route.
run_server starts a WebSocket server on 0.0.0.0:8080 and blocks until the process is killed. Can only be called once per process.
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 |
bytes | Bytes |
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 toolapiOr build from source:
wasm-pack build --target webCalling a Tool
import init, { call } from "toolapi"
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 } } |
Bytes | { "Bytes": Uint8Array } |
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.