Skip to main content

Tool system

Defining tools

Define tools using JSON Schema:
use meerkat::ToolDef;
use serde_json::json;

let tool = ToolDef {
    name: "get_weather".to_string(),
    description: "Get current weather for a city".to_string(),
    input_schema: json!({
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "City name"
            },
            "units": {
                "type": "string",
                "enum": ["celsius", "fahrenheit"],
                "default": "celsius"
            }
        },
        "required": ["city"]
    }),
};

Implementing AgentToolDispatcher

The AgentToolDispatcher trait connects your tools to the agent:
use async_trait::async_trait;
use meerkat::{AgentToolDispatcher, ToolCallView, ToolDef, ToolResult};
use meerkat::error::ToolError;
use serde_json::json;
use std::sync::Arc;

struct MyToolDispatcher;

#[async_trait]
impl AgentToolDispatcher for MyToolDispatcher {
    fn tools(&self) -> Arc<[Arc<ToolDef>]> {
        vec![
            Arc::new(ToolDef {
                name: "search".to_string(),
                description: "Search the web".to_string(),
                input_schema: json!({
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"]
                }),
            }),
        ].into()
    }

    async fn dispatch(&self, call: ToolCallView<'_>) -> Result<ToolResult, ToolError> {
        match call.name {
            "search" => {
                #[derive(serde::Deserialize)]
                struct Args { query: String }

                let args: Args = call.parse_args()
                    .map_err(|e| ToolError::InvalidArguments(e.to_string()))?;
                Ok(ToolResult::success(call.id, format!("Results for: {}", args.query)))
            }
            _ => Err(ToolError::not_found(call.name)),
        }
    }
}
For dynamic tool registration:
use meerkat::ToolRegistry;

let mut registry = ToolRegistry::new();

registry.register(tool_def, Box::new(|args| {
    Box::pin(async move {
        Ok("result".to_string())
    })
}));

let tools = registry.tools();

Multimodal tool results

Built-in tools return ToolOutput, which supports both JSON and multimodal content:
use meerkat::ToolOutput;
use meerkat::ContentBlock;

// Standard JSON result
let output = ToolOutput::Json(json!({"status": "ok", "count": 42}));

// Multimodal result with image content
let output = ToolOutput::Blocks(vec![
    ContentBlock::Text { text: "Screenshot captured:".to_string() },
    ContentBlock::Image {
        media_type: "image/png".to_string(),
        data: base64_encoded_png,
        source_path: Some("/path/to/screenshot.png".into()),
    },
]);
ToolOutput::Json is serialized into a text content block in the tool result. ToolOutput::Blocks passes content blocks directly into ToolResult.content, enabling tools to return images and other rich content to vision-capable models. The built-in view_image tool uses this mechanism to read image files from disk and return them as ContentBlock::Image blocks. It is automatically hidden from models that lack vision or image tool result support.

Session stores

File-based persistence using JSONL format:
use meerkat::JsonlStore;
use std::path::PathBuf;

let store = JsonlStore::new(PathBuf::from("./sessions"));
store.init().await?;

store.save(&session).await?;
let session = store.load(&session_id).await?;
store.delete(&session_id).await?;
Implement the SessionStore trait and pass it to AgentFactory::session_store(). Then build a runtime-backed SessionService so the custom store participates in the canonical session lifecycle instead of only a one-off direct agent.
use async_trait::async_trait;
use meerkat::{
    AgentFactory, Config, SessionStore, SessionFilter, StoreError, Session, SessionId,
    build_persistent_service, open_realm_persistence_in,
};
use meerkat_core::SessionMeta;
use meerkat_store::RealmBackend;
use std::sync::Arc;

struct BigQueryStore { /* your storage backend */ }

#[async_trait]
impl SessionStore for BigQueryStore {
    async fn save(&self, session: &Session) -> Result<(), StoreError> {
        // Persist session to BigQuery
        Ok(())
    }

    async fn load(&self, id: &SessionId) -> Result<Option<Session>, StoreError> {
        // Load session by ID
        Ok(None)
    }

    async fn list(&self, filter: SessionFilter) -> Result<Vec<SessionMeta>, StoreError> {
        Ok(vec![])
    }

    async fn delete(&self, id: &SessionId) -> Result<(), StoreError> {
        Ok(())
    }
}

// Wire into AgentFactory — all factory features (comms, hooks, skills, etc.) work unchanged
let store: Arc<dyn SessionStore> = Arc::new(BigQueryStore { /* ... */ });
let realms_root = std::env::current_dir()?.join(".rkat").join("realms");
let (_manifest, persistence) = open_realm_persistence_in(
    &realms_root,
    "team-alpha",
    Some(RealmBackend::Sqlite),
    None,
).await?;
let factory = AgentFactory::new(realms_root.clone())
    .runtime_root(realms_root)
    .session_store(store)
    .builtins(true)
    .shell(true);

let config = Config::load().await?;
let service = build_persistent_service(factory, config, 64, persistence);
// Sessions created through `service` now persist through BigQuery
Prefer SessionStore + session_store() over implementing AgentSessionStore directly. SessionStore is the richer trait (with list, delete, exists) and AgentFactory automatically wraps it via StoreAdapter. Implementing AgentSessionStore directly bypasses the factory and loses runtime-backed session orchestration.

MCP integration

Route tool calls across multiple MCP servers:
use meerkat::{McpRouter, McpServerConfig};
use std::collections::HashMap;

let mut router = McpRouter::new();

// Add stdio-based MCP server
let config = McpServerConfig::stdio(
    "my-server",
    "/path/to/mcp-server".to_string(),
    vec!["--arg".to_string()],
    HashMap::new(),
);
router.add_server(config).await?;

// Add HTTP/SSE-based MCP server
let config = McpServerConfig::http(
    "remote-server",
    "https://mcp.example.com/sse".to_string(),
);
router.add_server(config).await?;

// List all available tools
let tools = router.list_tools().await?;

// Call a tool
let result = router.call_tool("tool_name", &args).await?;

// Graceful shutdown
router.shutdown().await;

See also