Command line / Your own MCP server

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:

ConstructorFor a tool thatThe host sees
Tool::readsonly readsreadOnlyHint: true
Tool::writesadds or changes somethingreadOnlyHint: false, destructiveHint: false
Tool::removesdeletes something or replaces what it hadreadOnlyHint: 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:

--writeOffered
not giventhe tools that read
dashboard_savethose, and dashboard_save
dashboard_save,dashboard_deletethose, and both tools that write
allevery 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.

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

WhatValue
Transportstdin and stdout, one JSON-RPC 2.0 message per line
Protocol versionthe client's, or 2025-06-18 when it names none
Methodsinitialize, ping, tools/list, tools/call
A store tool's list200 items at most, logs 50 by default
Two tools with the same namerefused by serve with an Invalid error
A schema that isn't an objectrefused by serve with an Invalid error

See also

Edit this page on GitHub Updated 2 November 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results