# How to Register and Expose a Custom Tool via JSON-RPC in OpenHuman

> Learn to register and expose custom tools via JSON-RPC in OpenHuman. Implement the Tool trait, define schemas, and add handlers to enable seamless tool integration and execution.

- Repository: [Tiny Humans/openhuman](https://github.com/tinyhumansai/openhuman)
- Tags: how-to-guide
- Published: 2026-05-12

---

**To register a custom tool in OpenHuman, implement the `Tool` trait, define a `ControllerSchema` in your domain's [`schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/schemas.rs), and add the handler to `all_registered_controllers()` so the JSON-RPC dispatcher in [`src/core/jsonrpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/jsonrpc.rs) can route calls to your tool's `execute` method.**

The OpenHuman framework from `tinyhumansai/openhuman` treats every agent-visible capability as a **Tool** that communicates through a structured JSON-RPC interface. When you register and expose a custom tool via JSON-RPC in OpenHuman, you enable both UI components and LLM agents to discover and invoke your functionality through a standardized protocol. This registration process involves three core components: a trait implementation, a schema definition, and controller registration.

## Implement the Tool Trait

Start by creating a struct that implements the `Tool` trait defined in [`src/openhuman/tools/traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/traits.rs). This trait requires an `execute` method that accepts `serde_json::Value` and returns `anyhow::Result<ToolResult>`.

### Tool Structure and Execution Logic

Place your implementation in `src/openhuman/tools/impl/<category>/my_awesome_tool.rs`. The struct must handle deserialization of arguments and return a `ToolResult` via the `success` constructor.

```rust
use crate::openhuman::tools::traits::{Tool, ToolResult};
use anyhow::Result;
use serde_json::Value;

/// Arguments expected by the tool.
#[derive(serde::Deserialize)]
struct MyAwesomeArgs {
    text: String,
    repeat: usize,
}

pub struct MyAwesomeTool;

#[async_trait::async_trait]
impl Tool for MyAwesomeTool {
    fn name(&self) -> &str { "my_awesome_tool" }

    fn description(&self) -> &str {
        "Repeats a string a given number of times."
    }

    async fn execute(&self, args: Value) -> Result<ToolResult> {
        let params: MyAwesomeArgs = serde_json::from_value(args)?;
        let output = params.text.repeat(params.repeat);
        Ok(ToolResult::success(output))
    }
}

```

## Define the JSON-RPC Schema

Each tool needs a `ControllerSchema` entry in [`src/openhuman/tools/schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/schemas.rs) that maps the method name to a handler function. This schema includes the parameter specification using JSON Schema format and a pointer to the handler that bridges JSON-RPC to your tool.

### ControllerSchema Configuration

Add a match arm in the `schemas` function that returns a `ControllerSchema` with the method name, description, parameter properties, and handler reference.

```rust
use crate::openhuman::core::event_bus::{ControllerSchema, RegisteredController};
use crate::openhuman::tools::ops::run_tool;
use serde_json::json;
use std::collections::HashMap;

/// Register the method name and its JSON-RPC schema.
pub fn schemas(method: &str) -> ControllerSchema {
    match method {
        "my_awesome_tool" => ControllerSchema {
            function: "my_awesome_tool",
            description: "Repeat a string multiple times.",
            params: json!({
                "type": "object",
                "properties": {
                    "text": { "type": "string", "description": "Text to repeat" },
                    "repeat": { "type": "integer", "minimum": 1 }
                },
                "required": ["text", "repeat"]
            }),
            result: json!({ "type": "object" }),
            handler: handle_my_awesome_tool,
        },
        _ => panic!("unknown method"),
    }
}

```

### Handler Function Implementation

The handler function—typically named `handle_<tool_name>`—deserializes the incoming parameters and delegates to `run_tool` from [`src/openhuman/tools/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/ops.rs). This helper locates your tool by name and invokes its `execute` method.

```rust
/// The handler that bridges the JSON-RPC call to the tool.
pub fn handle_my_awesome_tool(
    params: crate::openhuman::core::event_bus::Map<String, crate::openhuman::core::event_bus::Value>,
) -> crate::openhuman::core::event_bus::ControllerFuture {
    // Convert the generic map into a serde_json::Value.
    let payload = crate::openhuman::core::event_bus::deserialize_params::<serde_json::Value>(params)?;
    // Run the generic tool runner – it will locate `MyAwesomeTool` by name.
    run_tool("my_awesome_tool", payload).map(crate::openhuman::core::event_bus::to_json)
}

```

## Register the Controller

Expose the method globally by appending a `RegisteredController` to the vector returned by `all_registered_controllers()` in [`src/openhuman/tools/schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/schemas.rs). This registration makes the tool visible to the dispatcher in [`src/core/jsonrpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/jsonrpc.rs).

```rust
pub fn all_registered_controllers() -> Vec<RegisteredController> {
    let mut controllers = vec![
        // … existing controllers …
    ];
    // Append our new controller.
    controllers.push(RegisteredController {
        function: "my_awesome_tool".into(),
        handler: handle_my_awesome_tool,
    });
    controllers
}

```

## Wire Into the Core Dispatcher

The [`src/core/all.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/all.rs) file aggregates controller lists from all domains. Ensure your domain's `all_registered_controllers()` is called here—the `openhuman::tools` domain is included by default. The RPC dispatcher automatically routes incoming `method` strings to the corresponding `RegisteredController` handler without requiring additional configuration.

## Test the Integration

Verify end-to-end functionality by adding a test in [`src/openhuman/tools/ops_tests.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/ops_tests.rs) that calls the method through the JSON-RPC client and asserts on the `ToolResult`.

```rust
#[tokio::test]
async fn jsonrpc_my_awesome_tool_works() {
    // Spin up the core side-car (or use the test harness).
    let client = crate::openhuman::core::jsonrpc::test_client().await;
    // Call the method via JSON-RPC.
    let resp = client
        .call(
            "my_awesome_tool",
            json!({ "text": "hi", "repeat": 3 })
        )
        .await
        .unwrap();
    // The result is a ToolResult serialized as JSON.
    let result: crate::openhuman::tools::traits::ToolResult = serde_json::from_value(resp).unwrap();
    assert_eq!(result.output(), "hihihi");
}

```

Run the suite with `cargo test -p openhuman-core` to confirm the tool responds correctly to JSON-RPC calls.

## Summary

- **Implement `Tool`**: Create a struct in `src/openhuman/tools/impl/<category>/` that implements the `Tool` trait with an `execute` method returning `ToolResult`.
- **Define schema**: Add a `ControllerSchema` entry in [`src/openhuman/tools/schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/schemas.rs) specifying the method name, JSON Schema parameters, and handler function.
- **Register handler**: Push a `RegisteredController` to `all_registered_controllers()` to expose the method to the JSON-RPC registry.
- **Route automatically**: The dispatcher in [`src/core/jsonrpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/jsonrpc.rs) handles routing without additional configuration.
- **Test thoroughly**: Use `cargo test -p openhuman-core` to verify the tool responds correctly to JSON-RPC calls.

## Frequently Asked Questions

### What is the Tool trait in OpenHuman?

The `Tool` trait defined in [`src/openhuman/tools/traits.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/traits.rs) is the core abstraction for agent-visible capabilities. It requires implementing `name()`, `description()`, and `execute()`, where `execute` receives JSON arguments and returns a `ToolResult` that gets serialized back to the caller.

### Where does the JSON-RPC routing happen?

Incoming JSON-RPC requests are routed in [`src/core/jsonrpc.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/jsonrpc.rs), which looks up the method name in the global registry of `RegisteredController` instances. When a match is found, it invokes the stored handler function, which typically delegates to `run_tool` in [`src/openhuman/tools/ops.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/ops.rs).

### How do I specify parameter schemas for my tool?

Parameter schemas are defined in the `ControllerSchema` struct within [`src/openhuman/tools/schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/openhuman/tools/schemas.rs) using `serde_json::json!` to create JSON Schema objects. Specify types, descriptions, and required fields in the `params` field to enable UI validation and LLM context window optimization.

### Can I register tools in a custom domain?

Yes. Create a new domain module with its own [`schemas.rs`](https://github.com/tinyhumansai/openhuman/blob/main/schemas.rs) file exporting `all_registered_controllers()`, then wire it into [`src/core/all.rs`](https://github.com/tinyhumansai/openhuman/blob/main/src/core/all.rs). Follow the same pattern: implement the `Tool` trait, define schemas, and register controllers. The core dispatcher aggregates all domains automatically.