Implementations
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 |
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
For client-only usage (e.g. in a CLI or script):
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> {
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 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
Note
The package is named toolapi on PyPI (not toolapi-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:
address: 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:
tool: 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:
Vec3 / Vec4
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:
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
Or build from source:
Calling 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.