croissant-mcp
# croissant-mcp
An MCP server that gives Claude programmatic access to a local
[En Croissant](https://encroissant.org) installation, so opening prep can be
driven from a conversation instead of by hand in the GUI.
## Quickstart
Requires Python 3.13+, [uv](https://docs.astral.sh/uv/), and a local
[En Croissant](https://encroissant.org) install (developed against 0.15.0).
```bash
git clone https://github.com/cheungbrenden/croissant-mcp
cd croissant-mcp
uv sync
```
Register the server once, user-level, so it's available in any session:
```bash
claude mcp add --scope user croissant-mcp -- uv run --directory /path/to/croissant-mcp croissant-mcp
```
Then ask for prep in a conversation:
> What am I due to review today, and what do opponents actually play there?
Nothing else to configure — the Lichess handle is derived from the
`<handle>_lichess.db3` filename and paths from the standard macOS locations.
[USAGE.md](USAGE.md) has the full tool list, with honest status marks for what
is built and what isn't.
## Why this exists
En Croissant is a Tauri desktop app. Its 64 backend commands are registered via
`tauri_specta::collect_commands!()` and reachable only over Tauri's IPC bridge —
there is no CLI, HTTP API, or headless mode. (`tauri_plugin_cli` is registered,
but its config declares a single positional `file` arg: that's file-association
handling, not a scripting interface.)
It doesn't need one. Everything the app persists is in open formats:
```
~/Library/Application Support/org.encroissant.app/
db/<name>.db3 SQLite — Games, Players, Events, Sites, Info
db/<name>.pgn the PGN the database was imported from
db/<name>.ecsi proprietary position index (magic bytes "ECSI", v4)
engines/stockfish/ the UCI engine binary
~/Documents/EnCroissant/*.pgn repertoire files
~/Library/WebKit/org.encroissant.app/.../LocalStorage/localstorage.sqlite3
FSRS training state, keyed deck-<repertoire path>-<n>,
values UTF-16. Not in the PGN.
```
So this project reads En Croissant's **files**, never its code. It is not a fork
and has no upstream relationship.
**Start with [USAGE.md](USAGE.md)** — what you can ask for and what's built yet.
Design records (build order, test seams, domain vocabulary) live in
[docs/](docs/).
**Developed against En Croissant 0.15.0.** The app registers
`tauri_plugin_updater` and updates itself, and every format above is internal and
undocumented. Startup asserts the shapes we depend on so a format change fails
loudly instead of being misread.
**The sidecar is not a live export.** It's the file the database was imported
from, so the two can drift: the Lichess pair matches exactly (190/190), the
chess.com pair does not (3525 PGN events against a `GameCount` of 3521). Treat
equivalence as an invariant to check, never a property to rely on. Because of
this — and because a game played ten minutes ago isn't in it at all — recent
games come from the Lichess API and the sidecar is the bulk/offline fallback.
## How repertoires and decks work
Read from En Croissant's own source rather than inferred by experiment.
**`Start` header** — `[Start "[0,0,0]"]` is a JSON array of child indices from the
tree root, written as `JSON.stringify(game.start)` and read as
`JSON.parse(Start ?? "[]")`. It marks where *training* begins, not where the file
begins: `buildFromTree` skips any node whose path is a prefix of `start`. The
Vienna file starts at `[0,0,0]`, which is why its deck drills from move 3
(`f4`, `e5`, `Nf3`) and never asks for `1.e4` or `2.Nc3`.
**Deck key suffix** — `deck-${file}-${game}`, where `game` is the index of the
game within the PGN file. `-0` is the first game; a multi-game repertoire gets
one deck per game.
**Cards are built eagerly, and adding lines is safe.** On open, if the deck is
empty En Croissant builds cards for the whole tree. If the deck is non-empty it
calls `syncDeck`, which reconciles against the tree, reports added/removed
counts, and **preserves existing cards and the review log**. So writing new lines
into a repertoire and reopening it schedules the new positions without
disturbing existing scheduling. Logs are only wiped by the explicit Reset button.
**A position's drilled answer is always its first child.** `buildFromTree` uses
`item.node.children[0].san` and ignores the rest. Whatever move we write first at
a node is the move you'll be drilled on — variation order is semantic, not
cosmetic.
Cards are only created for positions where it's your turn (halfmove parity
against `Orientation`), leaf nodes are skipped, and positions are deduplicated by
full FEN with the first occurrence winning — so transpositions collapse to one
card.
## Scope
Lichess only; chess.com is historical and opt-in. Blitz only by default, since
that's the bulk of most players' game history.
Your own win rate is **not** a ranking signal anywhere. A typical two-year window
holds only a few dozen blitz games per colour, which makes any per-position
percentage noise. Ranking is on coverage instead: deterministic, and it needs no
sample size.
## What it deliberately does not do
Two things on disk are closed formats:
- **The move BLOB.** `length(Moves)` equals `PlyCount` exactly — one byte per
ply, an index into the generated legal moves for each position. Decoding it
means reproducing En Croissant's move generator ordering byte-for-byte.
Avoided entirely by reading the `.pgn` sidecar instead.
- **The `.ecsi` index.** Powers fast position search over large databases. No
substitute here; brute-force PGN scanning covers the personal databases
(130 KB and 7.6 MB) and the Lichess explorer API covers reference statistics.
Indexed search over a multi-gigabyte master database is the one capability this
project cannot reach. If that ever becomes a real need, it's a separate project
(extract the Rust search core from En Croissant as a `[[bin]]` target) — not a
requirement here.
## What it does to your data
- **Read-only against `.db3`.** The `PawnHome` column and the `.ecsi` index are
derived; hand-written rows would desync them. To add games, write PGN and
import through the GUI.
- Repertoire files under `~/Documents/EnCroissant/` are the one place this
project writes at all.
## Development
```bash
uv sync
uv run pytest
```
TDQS
Scored across 8 tools
Each tool targets a distinct resource or action: databases vs. repertoires vs. training state vs. game queries. Even the three position-related tools differ clearly: one searches games, one reports user results, one reports opponent moves.
Most tools follow a verb_noun pattern (list_databases, read_repertoire, add_line_to_repertoire, get_training_status, search_games). Two tools break the pattern with possessive noun phrases (my_results_from_position, opponents_play), which is a minor inconsistency.
Eight tools is well within the ideal 3–15 range for a focused chess training server. Each tool addresses a distinct part of the workflow without unnecessary bloat.
The set covers listing, reading, and adding repertoire lines, plus training status and position analysis—solid coverage for the core domain. Missing explicit update/delete operations for repertoire lines are workable gaps but not fatal for typical use.