> Fetch the index of the TinyStore docs, every page as markdown, at https://tinyshed.org/tinystore/llms.txt

# Your own MCP server

The `mcp` feature of the Rust crate lets your program serve its own tools to
an AI agent over the Model Context Protocol, the way `tinystore mcp` does. A
tool reads by default, and a tool that writes is offered only when your
`--write` flag names it. You can also mount TinyStore's own read tools on your
store next to yours. This is Rust only for now: the Bun, Node, Python and Go
SDKs don't have it.

```sh
cargo add tinystore --features mcp
cargo add serde_json
```

```rust
use serde_json::json;
use tinystore::mcp::{self, Tool};

let store = tinystore::Store::open("data", Default::default())?;
let write = std::env::args().skip_while(|arg| arg != "--write").nth(1); // your --write flag
let name = json!({ "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] });

mcp::server("dashbin", env!("CARGO_PKG_VERSION"))
    .instructions("Read dashbin's dashboards and the store under them.")
    .tools(mcp::read_tools(&store).prefix("store_")) // store_logs, store_kv_get, …
    .tool(Tool::reads("dashboards_list", "The project's dashboards.", json!({ "type": "object" }), |_| {
        Ok(json!([{ "name": "latency" }]))
    }))
    .tool(Tool::writes("dashboard_save", "Saves a dashboard.", name.clone(), |args| {
        let name = args["name"].as_str().ok_or("dashboard_save takes name")?;
        Ok(json!({ "saved": name }))
    }))
    .tool(Tool::removes("dashboard_delete", "Deletes a dashboard.", name, |_| Ok(json!({ "deleted": true }))))
    .allow(write.as_deref()) // None, "all", or "dashboard_save,dashboard_delete"
    .serve()?; // answers stdin until it closes
```

An agent's host starts your program the same way it starts `tinystore mcp`:

```sh
claude mcp add dashbin -- dashbin mcp
claude mcp add dashbin -- dashbin mcp --write dashboard_save
```

## Tools and their marks

A tool is a value: its name, a description for the agent, the JSON Schema of
its arguments, and a closure that answers a call. The constructor you pick is
the tool's mark:

| Constructor     | For a tool that                           | The host sees                                   |
| --------------- | ----------------------------------------- | ----------------------------------------------- |
| `Tool::reads`   | only reads                                | `readOnlyHint: true`                            |
| `Tool::writes`  | adds or changes something                 | `readOnlyHint: false`, `destructiveHint: false` |
| `Tool::removes` | deletes something or replaces what it had | `readOnlyHint: false`, `destructiveHint: true`  |

The host reads the mark and asks its person before it runs a tool that writes
or removes, as Claude Code does for any tool not yet allowed. Every tool also
says `openWorldHint: false`.

The closure takes the call's arguments as a `serde_json::Value` and returns
`Ok` with JSON, which the agent reads as text. If it returns an error, the
call fails and the agent reads the error's message. So `?` works on any error
type:

```rust
Tool::reads("dashboard_get", "One dashboard.", name, |args| {
    let name = args["name"].as_str().ok_or("dashboard_get takes name")?;
    Err(format!("no dashboard {name}").into()) // the agent reads: no dashboard latency
})
```

## The allow list

`allow` takes your `--write` flag's value. A tool that writes or removes is
offered only when the list names it:

| `--write`                         | Offered                          |
| --------------------------------- | -------------------------------- |
| not given                         | the tools that read              |
| `dashboard_save`                  | those, and `dashboard_save`      |
| `dashboard_save,dashboard_delete` | those, and both tools that write |
| `all`                             | every tool                       |

- A tool the list doesn't name is not in `tools/list`, and a call of it is
  refused: `dashboard_delete writes, and dashbin mcp was not started with --write dashboard_delete`. Its closure doesn't run.
- A name that no tool that writes has is refused before the server answers
  anything. `serve` returns an `Invalid` error with the nearest name:
  `--write dashboard_sav: no tool that writes; did you mean dashboard_save?; all, or dashboard_save, dashboard_delete`.
- The refusals name `--write` and `<name> mcp`, so give your flag that name.
- The server adds one sentence to your instructions: which tools write, or
  `Nothing here writes.`

> \[!WARNING]
> A flag is no boundary for an agent that also has a shell. If the agent can
> run your program's commands itself, `--write` doesn't stop it. Give an
> agent `--write` where its host decides what it may run.

## TinyStore's tools on your store

`mcp::read_tools(&store)` gives the read tools of `tinystore mcp` for the
engines your program is built with: `logs`, `kv_get`, `kv_list`, `jobs_list`,
`sql_query` and `search_list`. They take the same arguments and answer the same
JSON as in [AI agents](https://tinyshed.org/tinystore/docs/guides/cli/mcp.md#tools). `prefix` renames them so that they don't
clash with yours:

```rust
.tools(mcp::read_tools(&store).prefix("store_")) // store_logs, store_kv_get, store_jobs_list, …
```

- Each call opens the store again beside your program, reads, and closes it.
  A call sees everything committed before it, and writes nothing. A
  misspelt bucket or queue name is an error, not a new empty bucket.
- `status` is not among them, because it describes a server and a directory.
  `tinystore mcp` adds it as its own tool.
- A tool that writes to the store is yours to write, with the mark it needs.

## Limits and defaults

| What                          | Value                                               |
| ----------------------------- | --------------------------------------------------- |
| Transport                     | stdin and stdout, one JSON-RPC 2.0 message per line |
| Protocol version              | the client's, or `2025-06-18` when it names none    |
| Methods                       | `initialize`, `ping`, `tools/list`, `tools/call`    |
| A store tool's list           | 200 items at most, `logs` 50 by default             |
| Two tools with the same name  | refused by `serve` with an `Invalid` error          |
| A schema that isn't an object | refused by `serve` with an `Invalid` error          |

## See also

- [AI agents](https://tinyshed.org/tinystore/docs/guides/cli/mcp.md): `tinystore mcp`, its tools and `--write`.
- [Let an AI agent read the store](https://tinyshed.org/tinystore/docs/handbook/production/running-it.md#let-an-ai-agent-read-the-store)
  in the Handbook.
