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.
cargo add tinystore --features mcp
cargo add serde_jsonuse 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 closesAn agent's host starts your program the same way it starts tinystore mcp:
claude mcp add dashbin -- dashbin mcp
claude mcp add dashbin -- dashbin mcp --write dashboard_saveTools 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:
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.
servereturns anInvaliderror 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
--writeand<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:
.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.
statusis not among them, because it describes a server and a directory.tinystore mcpadds 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:
tinystore mcp, its tools and--write. - Let an AI agent read the store in the Handbook.