Releases / TinyStore 0.7.0

TinyStore 0.7.0

TinyStore 0.7.0 came out on 3 November 2026. Config can keep secrets out of a settings file that is committed beside the code, and a setting can be declared with no value in every language. A Rust program's MCP server runs its calls side by side, stops a call that the agent's host cancels, and serves tools that await on the program's own async runtime. A Rust MCP tool's closure now takes the call's token, so the closures of 0.6.0 need a second argument.

Keep secrets out of a committed file

A settings file that is committed beside the code must not hold a password. Mark the file so that it refuses secrets, and each secret then has to come from the environment (#56):

const config = await store.config('dashbin', [decl, file('dashbin.toml', { secrets: false }), env('DASHBIN')])
config = await store.config("dashbin", [Settings, file("dashbin.toml", secrets=False), env("DASHBIN")])
config, err := tinystore.OpenConfig(ctx, store, "dashbin", defaults,
	tinystore.File("dashbin.toml", tinystore.NoSecrets()), tinystore.Env("DASHBIN"))
let config = store
    .config("dashbin", Settings::default())
    .file(File::from("dashbin.toml").secrets(false))
    .env(Env::prefix("DASHBIN"))
    .open()?;

If dashbin.toml gives a secret, the config doesn't open. The error names the variable to use instead, and never the secret's value:

config dashbin: dashbin.toml: db.password: a secret is not read from dashbin.toml; set DASHBIN_DB_PASSWORD or DASHBIN_DB_PASSWORD_FILE: invalid
  • A secret is refused wherever it stands: in a group, in an entry, or in a group that a setting chooses (contract CFG-16).
  • The error for a missing required secret names its variable and its _FILE, and no longer the file.
  • The mark is on the file, not on a setting. One mark keeps every secret out of the file, the secrets of a plugin's settings among them.
  • A file without the mark may hold secrets, as before.

Keep secrets out of a committed file has the details.

Settings with no value

Some settings have no sensible default: the password of a database that uses trust or peer authentication may stay unset. Declare such a setting with no value. It is null until a layer gives it, and a secret declared this way is still a secret (#57):

const config = await store.config('pg', [
	{ password: optional(secret(String)), port: optional(int({ min: 1 })) },
	file('pg.toml'),
	env('PG'),
])
@dataclass
class Pg:
    password: Secret | None = None
    port: Annotated[int, Range(min=1)] | None = None
type Pg struct {
	Password *tinystore.Secret `json:"password"`
	Port     *int              `json:"port" config:"min=1"`
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
struct Pg {
    password: Option<Secret<String>>,
    port: Option<u16>,
}
  • In a declaration as data, "default": null declares a setting with no value, of any kind: { "kind": "text", "secret": true, "default": null }.
  • A value that a layer gives is checked against the setting's kind and rules, like any other (contract CFG-15).
  • explain shows the setting as unset until a layer gives it a value.

Settings with no value has the details.

Slow calls and cancels

A Rust program's MCP server now runs each call on a thread of its own, so a slow tool holds up nothing. While it runs, the server answers ping, tools/list and other calls. When the agent's host cancels a call, the server cancels the call's token and writes no answer for it (#58):

rust
Tool::reads("dashboard_check", "Runs each panel's query.", panels, |args, token| {
    let mut checked = Vec::new();
    for panel in args["panels"].as_array().ok_or("dashboard_check takes panels")? {
        if token.is_cancelled() {
            return Err("cancelled".into()); // nobody reads it: the host gave up on the call
        }
        checked.push(run_query(panel)?);
    }
    Ok(json!(checked))
})
  • A read through a TinyStore store stops by itself with ErrorKind::Cancelled, the reads of mcp::read_tools among them (contract MCP-07).
  • Work outside the store stops only when your closure checks the token.
  • CancellationToken comes with the mcp feature alone, without a store.

Slow calls and cancels has the details.

Tools that await

With the mcp and async features, tinystore::aio::mcp serves tools whose closures return futures, so a program on tokio or another runtime awaits its own work in them, with no runtime of TinyStore's and no block_on:

rust
use tinystore::aio::mcp::{self, Tool};

mcp::server("dashbin", env!("CARGO_PKG_VERSION"))
    .tool(Tool::reads("query", "Rows of a read-only statement.", sql, move |args, _| {
        let pool = pool.clone();
        async move {
            let sql = args["sql"].as_str().ok_or("query takes sql")?;
            Ok(json!({ "rows": sqlx::query(sql).fetch_all(&pool).await?.len() }))
        }
    }))
    .serve()
    .await?; // answers stdin until it closes
  • Calls run side by side as futures that serve's own future polls. Nothing is spawned.
  • A cancelled call's future is dropped, and it gets no answer (contract MCP-08).
  • A blocking tinystore::mcp::Tool, mcp::read_tools among them, mounts on the same server, and each of its calls runs on a thread of its own.
  • With mcp and async and no engine, a program links no SQLite (contract MCP-09).

Async Rust has the details.

Changed names

  • Rust: the closure of mcp::Tool::reads, writes and removes takes the call's arguments and its &CancellationToken:

    rust
    Tool::reads("dashboards_list", "The project's dashboards.", schema, |_| Ok(json!([]))) // 0.6.0
    Tool::reads("dashboards_list", "The project's dashboards.", schema, |_, _| Ok(json!([]))) // 0.7.0
  • Rust: the async feature no longer turns on store. It awaits what is built beside it: the store and the engines you turn on, and the MCP server with mcp. Every engine turns on store, so a program with an engine changes nothing.

Fixes

  • In Rust, an Option<Secret<String>> left None is a secret. Before, a password that a file or a variable gave it printed in clear in explain and in errors.
  • In Rust, an Option<String> left None reads a variable as text. Before, APP_ID=123456 was read as a number, and the config failed to open.
  • tinystore config explain, set and reset read a config whose secret has a default. Before, they failed with "a setting has a default, or is required".
  • A tool whose closure panics fails its call with the panic's message, and the MCP server goes on. Before, the panic ended the server.
  • The sentence that the MCP server adds to its instructions says which tools change anything: Only dashboard_save changes anything; every other tool only reads. Before, a model read it as advice about the file.
  • A tool that removes and that --write doesn't name is refused as one that removes: dashboard_delete removes, and dashbin mcp was not started with --write dashboard_delete.

Install

bun add @tinyshed/tinystore@0.7.0
npm install @tinyshed/tinystore@0.7.0
pip install tinyshed-tinystore==0.7.0
go get tinyshed.org/tinystore@v0.7.0
cargo add tinystore@0.7.0
cargo install tinystore-cli@0.7.0

0.7.0 changes no file of a store, and 0.6.0 opens what 0.7.0 wrote. Only 0.6.0's tinystore config can't read a config that 0.7.0 kept with a setting of no value: use the 0.7.0 command line. If your program reaches a server, update the server with the SDK. A 0.6.0 server reads a secret from a file marked secrets: false, and refuses a setting declared with no value. The server image is ghcr.io/tinyshed/tinystore:0.7.0.

Every commit since 0.6.0 is in the GitHub release.

Edit this page on GitHub Updated 3 November 2026

esc
↑↓ navigate ↵ open ⇥ filter 0 results