---
title: "Tools and stores"
description: "Rust SDK reference for the tool system, session stores, and MCP integration."
icon: "screwdriver-wrench"
---

## Tool system

### Defining tools

Define tools using JSON Schema:

```rust
use meerkat::ToolDef;
use serde_json::json;

let tool = ToolDef::new(
    "get_weather",
    "Get current weather for a city",
    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:

```rust
use async_trait::async_trait;
use meerkat::{AgentToolDispatcher, ToolDef, ToolError, ToolResult};
use meerkat_core::{ToolCallView, ToolDispatchOutcome};
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::new(
                "search",
                "Search the web",
                json!({
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"]
                }),
            )),
        ].into()
    }

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

                let args: Args = call.parse_args()
                    .map_err(|e| ToolError::invalid_arguments("search", e.to_string()))?;
                Ok(ToolResult::new(call.id.to_string(), format!("Results for: {}", args.query), false).into())
            }
            _ => Err(ToolError::not_found(call.name)),
        }
    }
}
```

<Accordion title="ToolRegistry (dynamic registration)">
For dynamic tool registration:

```rust
use meerkat::ToolRegistry;

let mut registry = ToolRegistry::new();

registry.register(tool_def);

let weather = registry.get("get_weather");
let tools = registry.list();
```
</Accordion>

### Multimodal tool results

Tool results can carry either plain text or multimodal content blocks:

```rust
use meerkat::ToolResult;
use meerkat_core::{ContentBlock, ImageData};

// Standard text result
let output = ToolResult::new("tool-1".into(), "{\"status\":\"ok\",\"count\":42}".into(), false);

// Multimodal result with image content
let output = ToolResult::with_blocks("tool-2".into(), vec![
    ContentBlock::Text { text: "Screenshot captured:".to_string() },
    ContentBlock::Image {
        media_type: "image/png".to_string(),
        data: ImageData::Inline { data: base64_encoded_png },
    },
], false);
```

`ToolResult::new(...)` creates a text-only result. `ToolResult::with_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 the same content-block 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

<Tabs>
  <Tab title="JsonlStore">
    File-based persistence using JSONL format:

    ```rust
    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?;
    ```
  </Tab>
  <Tab title="MemoryStore">
    In-memory storage for testing:

    ```rust
    use meerkat::MemoryStore;

    let store = MemoryStore::new();
    store.save(&session).await?;
    let loaded = store.load(&session_id).await?;
    ```
  </Tab>
</Tabs>

<Accordion title="Implementing a custom store">
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.

```rust
use async_trait::async_trait;
use meerkat::{Session, SessionFilter, SessionId, SessionMeta, SessionStore, SessionStoreError};
use std::sync::Arc;

struct BigQueryStore { /* your storage backend */ }

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

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

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

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

// Use as a custom store implementation boundary
let store: Arc<dyn SessionStore> = Arc::new(BigQueryStore { /* ... */ });
```

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

For runtime-backed persistent services, realm persistence still flows through the
`PersistenceBundle` opened for that realm. Treat custom `SessionStore`
implementations as a storage seam for custom builders or direct agent/store
integration, not as a drop-in replacement for the entire runtime persistence
bundle.
</Note>
</Accordion>

---

## MCP integration

Route tool calls across multiple MCP servers:

```rust
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::streamable_http(
    "remote-server",
    "https://mcp.example.com/sse".to_string(),
    HashMap::new(),
);
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

- [Rust SDK overview](/rust/overview) - getting started, sessions, events
- [Rust SDK advanced](/rust/advanced) - expert-only direct agent construction, providers, hooks
- [Tools concept](/concepts/tools) - how the tool system works
