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

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

- **Config**
  - [Keep secrets out of a committed file](#keep-secrets-out-of-a-committed-file): a file marked `secrets: false` refuses a secret.
  - [Settings with no value](#settings-with-no-value): `null` until a layer gives one, a secret still masked.
- **MCP**
  - [Slow calls and cancels](#slow-calls-and-cancels): calls run side by side, and a cancelled one stops.
  - [Tools that await](#tools-that-await): `tinystore::aio::mcp` on your own runtime.

> \[!WARNING]
> **A Rust MCP tool's closure takes the call's token**
> In 0.6.0 a closure of `Tool::reads`, `Tool::writes` or `Tool::removes`
> took the call's arguments. It now takes the call's `CancellationToken`
> too. Add a second argument, `_` when your tool doesn't need it:
>
> ```diff
> - .tool(Tool::reads("dashboards_list", "The project's dashboards.", schema, |_| {
> + .tool(Tool::reads("dashboards_list", "The project's dashboards.", schema, |_, _| {
> ```

## 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](https://github.com/tinyshed/tinystore/issues/56)):

```ts
const config = await store.config('dashbin', [decl, file('dashbin.toml', { secrets: false }), env('DASHBIN')])
```

```python
config = await store.config("dashbin", [Settings, file("dashbin.toml", secrets=False), env("DASHBIN")])
```

```go
config, err := tinystore.OpenConfig(ctx, store, "dashbin", defaults,
	tinystore.File("dashbin.toml", tinystore.NoSecrets()), tinystore.Env("DASHBIN"))
```

```rust
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:

```text
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](https://tinyshed.org/tinystore/docs/guides/config/layers.md#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](https://github.com/tinyshed/tinystore/issues/57)):

```ts
const config = await store.config('pg', [
	{ password: optional(secret(String)), port: optional(int({ min: 1 })) },
	file('pg.toml'),
	env('PG'),
])
```

```python
@dataclass
class Pg:
    password: Secret | None = None
    port: Annotated[int, Range(min=1)] | None = None
```

```go
type Pg struct {
	Password *tinystore.Secret `json:"password"`
	Port     *int              `json:"port" config:"min=1"`
}
```

```rust
#[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](https://tinyshed.org/tinystore/docs/guides/config/secrets.md#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](https://github.com/tinyshed/tinystore/issues/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](https://tinyshed.org/tinystore/docs/guides/cli/mcp-server.md#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](https://tinyshed.org/tinystore/docs/guides/cli/mcp-server.md#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

```sh for=bun
bun add @tinyshed/tinystore@0.7.0
npm install @tinyshed/tinystore@0.7.0
```

```sh for=python
pip install tinyshed-tinystore==0.7.0
```

```sh for=go
go get tinyshed.org/tinystore@v0.7.0
```

```sh for=rust
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](https://github.com/tinyshed/tinystore/releases/tag/v0.7.0).
