Skip to content

Releases: CyberGitJul/gramps-remote-mcp

v0.5.1 — the cold wipe works

Choose a tag to compare

@CyberGitJul CyberGitJul released this 30 Jul 07:28
775b0da

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. fresh is 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 checks is_tree_disabled only at login, never per request, so it is the
    last point at which the deployment can refuse.
  • _login rides 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.
  • TokenRateLimitError subclasses requests.HTTPError, so the existing 502/503/504
    fallthrough and every other HTTPError handler 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_notes no 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
    TokenRateLimitError and 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 and TOKEN_RETRY_WAIT = 0.0 left the suite
    green; the orphan-note amplification above; a TokenRateLimitError message that
    claimed a retry had happened in the branch where none does).
  • Proven live against a real instance: gramps_delete_all_objects as the very
    first call
    of a fresh session, no warm-up — 391 objects → 0 in 7.7 s, no 429; then
    gramps_import_file restored all 391 with identical handles and change timestamps.
    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

Choose a tag to compare

@CyberGitJul CyberGitJul released this 29 Jul 12:51
0394cb6

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=1 gate. Both arguments are mandatory: expected_count must 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 letting expected_count=0 through
    without the tree's real size ever being established.
  • Completion is confirmed by polling object_counts to 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
    DeleteAllStateUnknownError and points at gramps_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_post now 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_file no longer reports a legitimately empty import as a timeout. An
    import that adds nothing now returns added: 0 once the counts have held steady for 30s,
    instead of running the full 300s and raising ImportTimeoutError for a success.
  • Packaging: mcp is now bounded to >=1.2.0,<2. mcp 2.0.0 removed mcp.server.fastmcp
    outright, which server.py imports, so an unbounded requirement makes docker build produce
    an image that builds fine and crashes on import. Raise the bound only together with the port
    to mcp.server.mcpserver.MCPServer — that is a real port, not a rename.
  • Docs/build hygiene: .env.example now 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. .dockerignore now 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), and import_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

Choose a tag to compare

@CyberGitJul CyberGitJul released this 23 Jul 08:18
4aa1bbe

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 + -v mount) — 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 synchronous 201 and Celery 202 deployments.
  • GrampsClient._request untouched — 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 includes backup_store.py.
  • Import requires the automation user to have OWNER role — run ops/setup-automation-user.sh with GRAMPS_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

Choose a tag to compare

@CyberGitJul CyberGitJul released this 20 Jul 16:08
ae71671

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. role is an explicit bloodline slot (never inferred from gender); returns the previous handle. Non-destructive (PUT).
  • gramps_remove_child_from_family — remove a child reference; raises ChildNotInFamilyError if the person isn't a child of that family. Non-destructive (PUT).
  • gramps_delete_person (destructive) — strict confirm is True plus a people-count guard; also cleans up notes orphaned by the deletion (shared notes are kept, reported via deleted_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 (keeps p/strong/a[href], strips script/img/on* handlers) — XSS-safe.

Name fields:

  • gramps_set_first_name (G12)
  • gramps_add_alternate_name (G13a) — generalizes the former add_birth_name, which stays as an alias.
  • gramps_swap_primary_name (G13b) — swaps primary_name with 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

Choose a tag to compare

@CyberGitJul CyberGitJul released this 19 Jul 10:35
72a12b8

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 bare pagesize defaults page to 1, since the upstream API only paginates when page >= 1).
  • gramps_get_ancestors(gramps_id, grade) — ancestor tree, mirror of get_descendants.
  • gramps_get_relations(gramps_id) — parent families (father/mother slots) + own families (partner + children); each person carries its own gender (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 in errors and does not abort the rest).

Fix

  • get_person now maps the live API's 404 on an unknown gramps_id to PersonNotFoundError.

Full PR: #1