Skip to main content
Glama
cheungbrenden

croissant-mcp

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

A4.3/5.0

Scored across 8 tools

Disambiguation5/5

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.

Naming Consistency4/5

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.

Tool Count5/5

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.

Completeness4/5

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.