Releases: CyberGitJul/gramps-remote-mcp
Release list
v0.5.1 — the cold wipe works
Patch release. One bug, found live against a real Gramps Web instance right after
v0.5.0 shipped: gramps_delete_all_objects failed on a cold client, which is
the normal case — a stdio MCP server is spawned per session.
The bug
Gramps Web rate-limits POST /api/token/ to one request per second per source IP
(@limiter.limit("1/second")). The wipe minted two tokens with only a single
metadata GET between them: the lazy login behind the precondition read, then an
unconditional fresh login for the endpoint's FreshProtectedResource. The second mint
came back 429 TOO MANY REQUESTS and the wipe died before the delete POST went out.
The tree was never touched by this failure — the 429 was raised from _login()'s
raise_for_status(), outside the try that guards the destructive POST. Verified
twice: by code order, and by a wire log.
The fix
- The fresh login before the POST is now conditional on this call not having minted
a token already.freshis a boolean JWT claim, not a time window, so a token minted
milliseconds earlier by the precondition read is already exactly what a second login
would produce. On a warm session the login still happens and still earns its mint:
Gramps Web checksis_tree_disabledonly at login, never per request, so it is the
last point at which the deployment can refuse. _loginrides out a single 429 and retries once, then raises the new
TokenRateLimitError. The limiter is keyed by IP, so a second MCP session or a
browser tab behind the same address can take the budget for the second this wipe needs
it — dropping our own second mint does not help there, only waiting does. Non-429
statuses still fail immediately and unretried: a 403 means the credentials are wrong,
and re-authenticating in a loop is what a brute-force guard counts.TokenRateLimitErrorsubclassesrequests.HTTPError, so the existing 502/503/504
fallthrough and every otherHTTPErrorhandler keep working. Its message scopes its
claim to the login itself rather than asserting nothing happened — it can surface
during the post-delete counts poll, where the wipe has already landed._delete_orphaned_notesno longer amplifies the retry. It swallows a per-note
transport error and carries on, which is right for a hiccup and wrong for a
client-wide rate limit: 20 orphaned notes meant 40 token POSTs and 22 s of blocking
sleep on the single-threaded stdio server, to fail anyway. It now breaks out on
TokenRateLimitErrorand reports what it managed to delete.
Verification
- 259 tests (243 → 259), 13 of the first 15 red before the fix — including one that
pinned the literal two-mint order (['login','request','login']). Mutation probe:
11 mutations, all red. - Adversarial review, 23 agents across 5 lenses: 17 findings, 16 refuted, 3 fixed here
(the retry wait was pinned by no test andTOKEN_RETRY_WAIT = 0.0left the suite
green; the orphan-note amplification above; aTokenRateLimitErrormessage that
claimed a retry had happened in the branch where none does). - Proven live against a real instance:
gramps_delete_all_objectsas the very
first call of a fresh session, no warm-up — 391 objects → 0 in 7.7 s, no 429; then
gramps_import_filerestored all 391 with identical handles andchangetimestamps.
Exactly the case that previously failed reproducibly. The instance uses
GRAMPSWEB_RATELIMIT_STORAGE_URI(Redis-backed limiter), so the test ran against the
strict configuration, not a lucky per-worker one.
No tool signatures changed; the tool count is unchanged at 27 + 4 destructive = 31.
v0.5.0 — Welle 6: tree wipe
Welle 6 — tree wipe. gramps_delete_all_objects empties a tree completely, so a reset to
exactly one source file is now export → wipe → import instead of one delete call per object.
- New destructive tool
gramps_delete_all_objects(confirm, expected_count)behind the existing
GRAMPS_ENABLE_DESTRUCTIVE=1gate. Both arguments are mandatory:expected_countmust equal
the tree's live total, so nothing is deleted unless the caller counted the tree first. A counts
read that comes back empty is refused as well, rather than lettingexpected_count=0through
without the tree's real size ever being established. - Completion is confirmed by polling
object_countsto zero, never/api/tasks/. Requires
OWNER role. The call can run for minutes, so the failure modes are classified rather than
guessed: a read timeout on the POST, or a 502/503/504 from a reverse proxy in front of a
synchronous deployment, falls through to the counts poll instead of being reported as a
failure — a connect timeout, where the request never landed, still fails fast, as does any
other status. If the delete was accepted but the counts cannot then be read, the call raises
DeleteAllStateUnknownErrorand points atgramps_get_object_counts: the counts, not the
response, are the authority on what happened to the tree. - Fix: orphaned-note cleanup after
gramps_delete_person/gramps_delete_blog_postnow skips
transport failures (connection reset, read timeout) as well as HTTP errors. Previously a
network blip while tidying up notes propagated out of a delete that had already succeeded,
so the caller got a bare network error instead of the before/after/deleted result this server
exists to guarantee. A failure that is not a failed request still propagates. Pre-existing
behaviour, not introduced by Welle 6. - Fix:
gramps_import_fileno longer reports a legitimately empty import as a timeout. An
import that adds nothing now returnsadded: 0once the counts have held steady for 30s,
instead of running the full 300s and raisingImportTimeoutErrorfor a success. - Packaging:
mcpis now bounded to>=1.2.0,<2. mcp 2.0.0 removedmcp.server.fastmcp
outright, whichserver.pyimports, so an unbounded requirement makesdocker buildproduce
an image that builds fine and crashes on import. Raise the bound only together with the port
tomcp.server.mcpserver.MCPServer— that is a real port, not a rename. - Docs/build hygiene:
.env.examplenow names all four tools the destructive gate unlocks — it
had omitted the wipe, which is the one with no undo, in the file you read while deciding
whether to enable the gate..dockerignorenow excludes.env*,.venv/and.git/rather
than the bare name.env. - Internals: the three 401→relogin branches were consolidated into one helper (with the
characterization tests they never had), andimport_file's polling loop was extracted as
_wait_for_counts, now shared with the wipe. CI runs the 243-test suite alongside ruff.
Tools: 27 with the destructive gate off, 31 with it on.
v0.4.0 — Backup / Restore
Welle 5 — Backup / Restore
Two new MCP tools for whole-tree backup and restore, moving files through a mounted backup directory (PR #6).
New tools (30 total with the destructive gate on / 27 off)
gramps_export_tree(filename=None, extension="gramps")— downloads the tree as a.gramps(gzip XML) backup into the backup directory. Read-only, always available. Returns{path, bytes, counts}.gramps_import_file(filename, extension="gramps")— imports a file from the backup directory into the tree (additive — Gramps import stacks, never merges). Returns{before, after, added}. Requires OWNER role at the REST layer.
Design
- Transport: mounted backup directory + file paths (
GRAMPS_BACKUP_DIR+-vmount) — no Base64 over MCP. - Async completion: derived from
object_counts(GET /api/metadata/), stabilized over consecutive polls. Never polls/api/tasks/(TTL-reaped). Works uniformly for synchronous201and Celery202deployments. GrampsClient._requestuntouched — dedicated raw binary helpers with the same auth + 401-relogin.- Path-traversal guard in a standalone
backup_store.py(rejects.., absolute paths, symlink/prefix escapes, null bytes).
Deployment
- Run the image with
-v <host>/export:/data -e GRAMPS_BACKUP_DIR=/data. The Docker image now includesbackup_store.py. - Import requires the automation user to have OWNER role — run
ops/setup-automation-user.shwithGRAMPS_ROLE=4.
Quality
208 tests pass (15 new), ruff clean, adversarial multi-agent review across four dimensions (polling correctness, traversal security, server-glue/spec, test gaps): 0 confirmed findings.
Full changelog: v0.3.0...v0.4.0
v0.3.0 — structure-edit, destructive & blog CRUD tools
Highlights
This release grows the server from 16 to 28 MCP tools — 25 always-on plus 3 destructive tools behind an opt-in gate — adds a full blog-post CRUD surface, and introduces a ruff lint/format gate across the codebase. 193 tests green.
Wave 3 — structure-edit & destructive tools (#2)
gramps_set_family_parent— set the father/mother handle of an existing family.roleis an explicit bloodline slot (never inferred from gender); returns the previous handle. Non-destructive (PUT).gramps_remove_child_from_family— remove a child reference; raisesChildNotInFamilyErrorif the person isn't a child of that family. Non-destructive (PUT).gramps_delete_person(destructive) — strictconfirm is Trueplus a people-count guard; also cleans up notes orphaned by the deletion (shared notes are kept, reported viadeleted_notes).gramps_delete_family(destructive) — families-count guard; refuses to delete while children are still attached (FamilyNotEmptyError).
Destructive tools are hidden unless GRAMPS_ENABLE_DESTRUCTIVE=1 is set — they are not registered at all in the default deployment.
Wave 4 — blog CRUD & name fields (#3)
Blog posts (a Gramps Source tagged Blog with an HTML body note):
gramps_create_blog_post,gramps_list_blog_posts,gramps_get_blog_post,gramps_update_blog_post(partial read-modify-write, type-preserving),gramps_delete_blog_post(destructive, env-gated, orphan-note cleanup).- Body rendering is controlled by
GRAMPS_BLOG_BODY_FORMAT(html|text). HTML is sanitized through a bleach allow-list (keepsp/strong/a[href], stripsscript/img/on*handlers) — XSS-safe.
Name fields:
gramps_set_first_name(G12)gramps_add_alternate_name(G13a) — generalizes the formeradd_birth_name, which stays as an alias.gramps_swap_primary_name(G13b) — swapsprimary_namewith an alternate name by index.
Tooling & fixes
- Lint gate: ruff lint + format as a GitHub Action, pre-commit / pre-push hooks, and a required merge check on
main(#3). - Docker fix: the image now copies
gramps_blog.py, so the container no longer crashes on import (#4).
Known limitation
Flipping GRAMPS_BLOG_BODY_FORMAT between html and text on an existing post can render the stored body as an escaped literal — see docs/blog-crud.md §7.1.
Full changelog: v0.2.0...v0.3.0
v0.2.0 — read + bulk-write tools
Six new MCP tools (10 → 16) plus a search extension and one fix. All test-driven (112 tests green) and verified end-to-end against a live Gramps Web 6.0.8 instance, with a pre-merge adversarial multi-agent review.
Read tools
gramps_get_object_counts()— flat object counts (people/families/events/…), useful as a before/after guard.gramps_list_people(keys, page, pagesize)— field selection + pagination (a barepagesizedefaultspageto 1, since the upstream API only paginates whenpage >= 1).gramps_get_ancestors(gramps_id, grade)— ancestor tree, mirror ofget_descendants.gramps_get_relations(gramps_id)— parent families (father/mother slots) + own families (partner + children); each person carries its owngender(father/mother are bloodline slots, not sex).gramps_search_person(query, limit)— now also matches the combined "First Surname", the nickname, and alternate/maiden names.
Write tools
gramps_set_gender_bulk/gramps_set_surname_bulk— batch updates under a single record-count guard; best-effort (a failing item is reported inerrorsand does not abort the rest).
Fix
get_personnow maps the live API's 404 on an unknowngramps_idtoPersonNotFoundError.
Full PR: #1