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

To register a custom tool in OpenHuman, implement the Tool trait, define a ControllerSchema in your domain's schemas.rs, and add the handler to all_registered_controllers() so the JSON-RPC dispatcher in 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. 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.

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

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. This helper locates your tool by name and invokes its execute method.

/// 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. This registration makes the tool visible to the dispatcher in src/core/jsonrpc.rs.

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 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 that calls the method through the JSON-RPC client and asserts on the ToolResult.

#[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 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 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 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, 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.

How do I specify parameter schemas for my tool?

Parameter schemas are defined in the ControllerSchema struct within 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 file exporting all_registered_controllers(), then wire it into src/core/all.rs. Follow the same pattern: implement the Tool trait, define schemas, and register controllers. The core dispatcher aggregates all domains automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →