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.

ImplementationPackageRole
Rusttoolapi on crates.ioCore: defines types, protocol, server + client
Pythontoolapi on PyPIClient with idiomatic Python value classes
JavaScript / WASMtoolapi-wasm on npmClient-only, async call() for web apps

Rust

GitHub | crates.io | version 0.4.5 | License: AGPL-3.0

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

FeatureDescription
serverAxum-based WebSocket server (native only)
clientWebSocket client (tungstenite on native, ws_stream_wasm on wasm32)
pyo3PyO3 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 (pass Some(INDEX_HTML) as second argument)
  • /tool accepts 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

TypeDescription
ValueDynamic typed enum — the core data type exchanged between tool and client
MessageFndyn FnMut(String) -> Result<(), AbortReason> — send progress, detect abort
ToolFnfn(Value, &mut MessageFn) -> Result<Value, ToolError> — tool signature
ToolErrorError returned by a tool: Extraction, Abort, or Custom(String)
ToolCallErrorClient-side error from call(): connection, protocol, or tool errors

Python

GitHub | PyPI | version 0.4.5 | License: AGPL-3.0

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 toolapi

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:

def call(
    address: str,
    input: Value,
    on_message: Callable[[str], bool] | None = None,
) -> Value
  • address: WebSocket URL of the tool server
  • input: Any Python object that maps to a ToolAPI Value (see below)
  • on_message: Optional callback for progress messages; return False to 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:

PythonToolAPI Value
NoneNone
boolBool
intInt
floatFloat
strStr
complexComplex
dictDict (heterogeneous) or TypedDict (homogeneous)
listList (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, InstantSeqEvent

Vec3 / 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

GitHub | npm | version 0.4.5 | License: AGPL-3.0

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-wasm

Or build from source:

wasm-pack build --target web

Calling 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 Promise that resolves to the result or rejects with an Error
  • The on_message callback is called for each progress message; return false to 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 typeJavaScript 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.