gramps-evidence-mcp
gramps-evidence-mcp is an MCP server that gives an AI assistant read/write access to a Gramps Web genealogy tree with an evidence-first model (every fact carries a citation), plus read-only audits and untrusted legacy-GEDCOM hints.
Create records: people (with cited birth/death), families (parents, children, marriage), events on people/families, shared event references in a role, alternate names, sources, citations, repositories, places, notes, media uploads, attributes, URLs, tags, and DNA matches.
Enforce evidence discipline: facts are written with attached citations by default;
require_citation=falsestamps factsUNSOURCED; parent-child links get their own citation viacite_child_linkbecause one citation carries one confidence;uncitedetaches and deletes orphaned citations.Cite things: attach citations to events, families, names, or any object; cite a parent-child link; create standalone reusable citations.
Edit records in place: update events, citations (single or bulk sweep), sources, media, people (gender, primary name, privacy), alternate names, child relationships, family parents, event references, attributes, URLs, places (type, enclosure, coordinates), and arbitrary scalar fields.
Restructure: link sources to repositories, move or detach children, merge duplicates (dry-run by default, server-side transactional merge), detach references, delete objects with orphan guards.
Read one record: shaped getters for person, family, event, source, repository, place, citation, note, media, plus a raw
get_objectfor any type.Read the tree: ancestor/descendant walks, name search, timelines (single or consolidated), relationship calculation, living/dead assessment, event-to-event spans, record-holder demographics, researcher details, tags, object/event type vocabularies.
Run reports: list Gramps' reports, read a report's default options, and generate one.
Audit the tree: server-side GrampsQL filtering (
query_objects) and structured queries (query_records, the only way to filter events by type), backlinks, duplicate detection, unsourced-fact listing, family-link checks, Gramps plausibility checks (verify_tree), and DB stats.Manage Gramps' filter vocabulary: list filter rules, list/save/delete custom filters.
Handle DNA: record matches with parsed segments, list matches with shared cM, read Y-DNA haplogroups, parse pasted segment data.
Manage research tasks: add, list, and update Gramps Web's own task list (status, priority, attributes, descriptions).
Operate the server: full-tree backup export, transaction listing/inspection, conflict-checked undo, per-record change history, background job/task polling, and search-index rebuild.
Read documents: OCR a media object server-side (Tesseract) as a finding aid, never as evidence.
Consult legacy GEDCOMs: read-only search over configured Ancestry/FamilySearch exports returning untrusted hints, each fact flagged as sourced or unsourced.
Filter privacy by default: bulk outputs redact private and probably-living people unless
include_privateis passed; single-record lookups and all writes are unfiltered.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@gramps-evidence-mcp add Mary Walsh's birth fact, cited to the 1900 census"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
gramps-evidence-mcp
An MCP server that gives an AI assistant read/write access to a Gramps genealogy tree, plus a read-only "reference layer" over legacy GEDCOM exports.
97 tools, built around evidence discipline. The premise is that an assistant
turned loose on a family tree will happily invent a plausible ancestor, so the
write paths here are shaped to make every claim carry its source: facts are
created with citations attached, uncite deletes what it orphans, parent-child
links are cited independently because one citation object cannot carry two
confidences, and an audit set — list_unsourced_facts, get_backlinks,
find_duplicates, and server-side queries through query_objects and
query_records — exists to find the places where that discipline slipped.
Legacy trees (Ancestry / FamilySearch exports) are consulted through the read-only reference layer as untrusted hints, never a source of truth.
The server is a REST client of Gramps Web
(gramps-webapi). It never touches the Gramps database files directly — see
How it connects.
Living-person privacy filtering is on by default — see Privacy.
This is an independent project. It is not made, endorsed or supported by the Gramps project.
Contents
Related MCP server: Gramps MCP
How it connects
The server speaks HTTP to gramps-webapi and never opens the Gramps database
files. The API server is the sole owner of the database, so this server and the
Gramps Web frontend can both write without contending for the desktop app's
exclusive lock. The package imports no gramps.gen.* module and needs no
sys.path surgery.
One caveat: side by side means the Gramps Web frontend and this server, both
talking to the same gramps-webapi. It does not mean pointing the Gramps
desktop app at the same database file that gramps-webapi is serving -- that
reintroduces the two-writer problem. Treat the Gramps Web tree as the source of
truth and round-trip with the desktop app through export/import.
See docs/ARCHITECTURE.md for the layer breakdown.
Setup
You need a running Gramps Web instance, and
uv to run the server.
Option A — you already run Gramps Web
If Gramps Web is already up anywhere you can reach, you don't need the bundled docker-compose. Just point the MCP at it:
Note the API base URL, e.g.
http://gramps.example.org:5000. The REST API lives under…/api.Create a dedicated MCP user with at least the editor role (writes need editor; owner also works). From a shell on the Gramps Web host / container:
gramps-web user add mcp 'A_STRONG_PASSWORD' --fullname 'MCP client' --role 3(On the official image the wrapper is
gramps-web; the underlying command ispython3 -m gramps_webapi user add ….)Put the URL + credentials in your environment (see Configuration).
Option B — run Gramps Web locally with the bundled compose
A ready-to-go stack is in docker/docker-compose.yml:
SQLite backend, bind-mounted data and media directories (a plain directory
tree you can copy or back up like any other), and host port 5555 → container
5000. Port 5555 avoids macOS's AirPlay Receiver, which occupies 5000.
docker compose -f docker/docker-compose.yml up -dThen create the first owner (browser wizard at http://localhost:5555, or CLI) and the dedicated MCP editor user:
docker compose -f docker/docker-compose.yml exec grampsweb \
gramps-web user add owner 'OWNER_PW' --fullname 'Owner' --role 4
docker compose -f docker/docker-compose.yml exec grampsweb \
gramps-web user add mcp 'MCP_PW' --fullname 'MCP client' --role 3The tree named by GRAMPSWEB_TREE (default MyTree) is created on first start.
Install the MCP server
uvx gramps-evidence-mcpThat runs the server over stdio, which is how an MCP client starts it. You normally put it in the client's configuration (below) rather than running it yourself.
Configuration
Secrets live in the environment; everything else in a TOML file. The TOML file therefore never contains credentials.
Environment variables
Set them in the client configuration, or in a .env file in the directory the
server starts in (see .env.example). Real environment
variables win over the file.
Var | Meaning |
| Base URL of |
| The dedicated MCP user |
| Its password |
| Path to the TOML config (default |
| Override the TOML |
|
|
| Where |
| Optional. A Transkribus account (Scholar plan or above), for |
| Pages a calendar month |
| Transkribus' processing API (default |
TOML file (see gramps_mcp.example.toml)
[server]
expose_private = false # see Privacy below
[[reference]]
path = "~/gedcoms/export_one.ged"
label = "Ancestry (one branch)"
trust = "User-built; many hints unsourced. Verify everything."A desktop client starts the server in a directory of its own choosing, so give
GRAMPS_MCP_CONFIG as an absolute path.
Client configuration
Claude Desktop — add to claude_desktop_config.json (on macOS,
~/Library/Application Support/Claude/):
{
"mcpServers": {
"gramps": {
"command": "uvx",
"args": ["gramps-evidence-mcp"],
"env": {
"GRAMPS_MCP_API_URL": "http://gramps.example.org:5000",
"GRAMPS_MCP_USERNAME": "mcp",
"GRAMPS_MCP_PASSWORD": "MCP_PW",
"GRAMPS_MCP_CONFIG": "/path/to/gramps_mcp.toml"
}
}
}
}Claude Code:
claude mcp add gramps \
-e GRAMPS_MCP_API_URL=http://gramps.example.org:5000 \
-e GRAMPS_MCP_USERNAME=mcp -e GRAMPS_MCP_PASSWORD=MCP_PW \
-e GRAMPS_MCP_CONFIG=/path/to/gramps_mcp.toml \
-- uvx gramps-evidence-mcpAny other MCP client that launches stdio servers works the same way: the
command is uvx gramps-evidence-mcp, with the variables above in its
environment. Restart the client after editing its configuration.
To run from a clone instead, replace uvx gramps-evidence-mcp with
uv --directory /path/to/gramps-evidence-mcp run gramps-evidence-mcp.
Remote access (Claude web / mobile)
The configuration above uses the stdio transport: the client launches the server as a local subprocess. Claude web (claude.ai) and the mobile apps can't do that — they connect to a remote server over HTTPS using the Streamable HTTP transport, added as a Custom Connector. Three differences to plan for:
Transport must be HTTP, not stdio.
Reachability — Anthropic's servers dial out to yours, so it needs a public HTTPS URL. A LAN address like
http://192.168.1.50:5000won't work.Auth — it's write access to a database of living relatives, so it must sit behind authentication. The server has none of its own. Never expose it authless.
If you don't specifically need a browser/phone, a desktop client already gives you the same models with this server working locally (stdio, no public exposure). The steps below are only for genuine remote access.
1. Serve it over HTTP
GRAMPS_MCP_TRANSPORT=http uvx gramps-evidence-mcpThat serves the MCP endpoint at http://127.0.0.1:8090/mcp; GRAMPS_MCP_HOST
and GRAMPS_MCP_PORT change where. The other GRAMPS_MCP_* variables apply as
before.
Bound to a loopback address, the server accepts only requests whose Host is
localhost or 127.0.0.1 — the MCP SDK's protection against DNS rebinding. A
tunnel or proxy on the same machine must therefore send Host: localhost
(cloudflared's httpHostHeader setting does this). In a container, bind
0.0.0.0 instead, as below, and let the container network do the isolating.
2. Deploy it next to Gramps Web
Run it in a container on the same host as Gramps Web so it talks to the API
over the internal network. A minimal Dockerfile:
FROM python:3.13-slim
RUN pip install uv
ENV GRAMPS_MCP_TRANSPORT=http GRAMPS_MCP_HOST=0.0.0.0 GRAMPS_MCP_PORT=8090
EXPOSE 8090
CMD ["uvx", "gramps-evidence-mcp"]Add it to your Gramps Web compose (same network), pointing at the API by service name and reading secrets from the environment:
gramps_evidence_mcp:
build: /path/to/dockerfile-dir
restart: unless-stopped
environment:
GRAMPS_MCP_API_URL: "http://grampsweb:5000" # internal service name
GRAMPS_MCP_USERNAME: "mcp"
GRAMPS_MCP_PASSWORD: "${MCP_PW}"
# no host port needed if the tunnel (below) runs in the same compose network3. Expose it publicly with TLS
A reverse tunnel avoids port-forwarding and gives you TLS and a stable hostname. Any of these work:
Cloudflare Tunnel — run
cloudflaredalongside the server and route a hostname such ashttps://gramps-mcp.example.org→http://gramps_evidence_mcp:8090.Tailscale Funnel — public HTTPS over your own tailnet.
ngrok http 8090for a quick throwaway test.
Or terminate TLS yourself with any reverse proxy in front of port 8090.
4. Put auth in front
claude.ai custom connectors speak the MCP OAuth 2.0 flow. Put an identity proxy in front of the tunnel hostname — Cloudflare Access, Authelia, oauth2-proxy and similar all tie into the OAuth flow the connector expects. The server does not authenticate callers itself; see docs/ROADMAP.md.
5. Add the connector in claude.ai
Settings → Connectors → Add custom connector → paste
https://gramps-mcp.example.org/mcp, complete the auth prompt, and the 97 tools
appear in chat. (Custom connectors require a paid Claude plan; on
Team/Enterprise an admin may need to enable them.)
Security reminder: this endpoint can create, edit, and delete records in a tree containing living people. Keep it behind auth, prefer a private tunnel over an open port, and consider a Gramps Web user with a read-only role if you only need lookups remotely — the server's write tools then fail with a permission error instead of writing.
The evidence model & transactions
The server enforces the Gramps evidence model:
Repository → Source → Citation → fact (Event / Attribute)Every fact-recording write requires a citation. You either reference an
existing citation/source or create one inline. The only way to record a fact
without a citation is to explicitly pass require_citation=False, which stamps
the event with an UNSOURCED=true attribute so list_unsourced_facts can
find it later. Confidence levels map to Gramps' 0–4 scale
(very_low, low, normal, high, very_high).
Type names are Gramps' own. Gramps stores any type name it does not know
exactly — "birth", "Web Home Page" — as a new custom type, beside the
standard one it was meant to be, and keeps it in the tree's vocabulary for
good. So every write matches a type name first: against Gramps' standard names
and the tree's custom ones, ignoring case and punctuation, then through a few
unambiguous synonyms ("Born" is Birth, "microfilm" is Film). A name that still
matches nothing is refused with the closest names; allow_new_type creates it
when a new custom type is meant ("Land Grant", a "Territory" place).
docs/PITFALLS.md section 26 has the details.
How writes map to DbTxn transactions
gramps-webapi wraps every object create/update in its own server-side DbTxn
(labelled New Person, Edit Event, …), so all writes go through proper
transactions and stay in Gramps' undo history. A composite operation
(e.g. add_person with a cited birth) is performed as an ordered sequence of
these object writes — source → citation → event → person — with references wired
up as it goes.
Design note (documented deviation): gramps-webapi also offers a raw
POST /api/transactions/ endpoint that can bundle several objects into a single
DbTxn with a custom description. We intentionally don't use it, because
that endpoint bypasses the server-side helpers that (a) auto-assign handle and
gramps_id and (b) coerce English type strings ("Birth") into internal Gramps
type dicts. Using it would force this client to reimplement Gramps' ID allocation
and type internals and to invent gramps_ids, risking a desynced ID counter. The
per-object-transaction approach is safer and keeps undo coherent; the trade-off
is that one logical add appears as a few undo entries rather than one.
Privacy
The tree contains living people. With expose_private = false (the default),
bulk output leaves out anyone private or probably living:
Probably living means born less than 110 years ago with no recorded death — or with neither a birth nor a death recorded, which errs toward privacy. A death event counts as recorded even without a date. 110 is the conventional genealogical "presumed dead" cutoff; it does not permanently hide clearly historical people.
Private means the Gramps private flag, on a record of any type.
What that means tool by tool:
Output | A private or probably-living person… |
| …appears as a redacted stub: ids only, no name or facts, so it can still be fetched deliberately. A |
| …has their facts left out and counted, and appears once as a stub. |
| …is left out. A namesake group is reported only while two historical people remain in it. |
| …has their events left out and counted when they are folded in as a relative. The people you name are shown; a family's own members are judged like relatives, so a living couple's family timeline comes back empty. |
| …is excluded by the server before it computes anything. |
| …is left out of any report that has the options for it — 21 of Gramps' 25 reports have |
| …is left out of the matches and counted. Exports from Ancestry and similar sites privatize nobody. |
| …has the link problems found on them left out and counted. |
A lookup by id is not filtered. get_person, get_object, the other typed
getters, and list_unsourced_facts for one named person answer in full. This is
a personal tool the tree's owner runs against their own data: the goal is to
prevent accidental bulk leakage — search dumps, tree walks, shared reports —
not to lock the owner out. Asking for one record by id is a deliberate act.
Writes are never filtered. Adding, citing, editing, merging and deleting work on living and private people like anyone else.
Ask, and one call shows everyone. Every tool in the table takes
include_private. Passed as true, that call answers in full — living people,
private records, Gramps' own report defaults — and the next call is filtered
again. It is meant for when you ask for living relatives by name ("list my
cousins born after 1950"), and the tool descriptions tell the assistant so.
Each use is logged with the tool's name, never the records.
Not filtered, by design or by limitation:
export_backupis a lossless backup, and a backup that drops living people cannot restore the tree.verify_treereturns Gramps' own findings as it words them.get_dna_matchesanswers for the person you name, but each match is another person — usually a living one — identified by handle, with segment data.Events, citations, notes and media are judged by their own private flag only. An event row carries no link back to its person, so a living person's birth event is visible to an event query unless the event itself is private.
list_research_tasksjudges a task by its own private flag, and its description by its note's. A task's title can name a living person; mark such a task private.Timeline entries are judged by their person, not by the event's own private flag, which the timeline endpoint does not report.
ocr_mediareads the media object you name, as a lookup by id does, private or not. It never sends a private one to Transkribus, a third party; a public image of a living person's record would go, so mark such a record private.A stub still says that a record matched. A query for a name and a birth year that returns a stub confirms that the id is a person fitting both. The filter prevents accidental disclosure, not a determined search.
To turn the filter off for every call instead, set expose_private = true in
the TOML file or GRAMPS_MCP_EXPOSE_PRIVATE=true in the client's configuration
— for a full audit, or a tree with no living people in it. That reduces
privacy protection for everything the assistant reads.
Logs never contain record contents — only handles, ids, and operation names. Request URLs are kept out of the log too, because a query filter travels in one.
Tool reference
97 tools. Every tool that mutates the tree re-fetches the whole object
before PUTting it back — edits through service._mutate() — see
the keys= trap.
Every tool declares MCP annotations saying whether it only reads, adds, or
changes and removes, so a client can approve reads automatically and ask before
the rest. 45 tools only read. ocr_media reads, but is annotated as neither
read-only nor closed-world: it can fetch from the Library of Congress and the
Internet Archive, spend Transkribus credits, and store a transcript note.
Unknown parameters are refused. A misspelt or invented argument is an error
that lists the parameters the tool does take. It is not silently dropped, which
used to turn query_objects(query=...) — the parameter is gql — into an
unfiltered listing of the first 200 objects.
Create
Tool | Purpose |
| Create a person, optionally with cited birth/death events; each event's type may be left out. |
| Link parents + children with an optional cited marriage. A child may be given with its relationship to each parent — a stepchild in the same call. |
| Add a cited event (residence, census, occupation…) to a person. |
| Add a dated/placed event (marriage, divorce…) to a family. |
| Add an existing person as a child of an existing family. |
| Share an existing event with another person, in a role (Witness, Informant, Godparent…): one census entry or burial, one set of citations. What is the person's own — their line on the sheet, their age — goes on the reference as attributes ( |
| Add a non-primary name (AKA, married name, nickname) to a person, with the citation for that form of the name. |
| Create a Source (record set, book, certificate), optionally in a repository. |
| Create/reuse a standalone Citation on a Source. |
| Create a Repository (archive, library, cemetery, website). |
| Create a place deliberately, typed and parented, instead of letting one appear as a side effect of naming it in an event. |
| Create a note, optionally attached to an object. Re-reads the target and returns |
| Upload an image, PDF, audio or video file as a standalone Media object; reuses an identical file by md5. |
| Link a file (uploading it) or an existing Media object to an object. |
| Add a typed key/value attribute ( |
| Add a web URL to a person, place or repository. |
| Record a DNA match as evidence: stored as Gramps Web stores one, cited to the test that found it. Refuses segment data that does not parse, and a second record of the same pair. |
Cite — attaching evidence to a claim
Tool | Purpose |
| Attach a citation to an event that already exists. |
| Attach a citation to any object that carries one — notably a family, and one of a person's names ( |
| Cite the parent-child link itself. Always mints the link its own citation, because one citation object cannot carry two different confidences. |
| Detach a citation, deleting it if that leaves it orphaned. Always delete — detach-without-delete has produced orphan debris twice. A citation that is the only holder of a note or image is kept instead, unless |
Edit — correcting what is already there
Tool | Purpose |
| Locator, confidence, date — or re-point the citation at a different source. |
| The same for many citations in one call — a sweep. |
| Type (checked against the tree's vocabulary), date, place, description, in place, keeping the event's id and everything attached to it. Clears a place or date no source states. |
| Title, author, pubinfo, abbrev. |
| Description, date, path. |
| Gender, primary name (the old one is kept as an alternate, unless it was a data-entry error — then it is corrected in place and the reason recorded in a note), privacy flag. |
| Correct, retype or remove one alternate name in place, keeping its citations. A cited name is not removed. |
| A child's relationship to the father or mother — Birth, Stepchild, Adopted… — in place, keeping the link's citations and the birth order. |
| Move a child to another family, keeping the link's citations, notes and privacy, placed in birth order; every link back to the old family goes, a duplicated one too. |
| Set, replace (when asked) or remove the father or mother of an existing family, keeping both sides of each person's link. |
| A person's reference to a shared event, in place: the role, or an attribute set, added or removed by name, keeping its citations. |
| Set or remove ONE attribute on a person, family, event, media object, source or citation, in place, keeping its citations; picked by name, and by part of its value where the name repeats. A removed attribute's citations are named. |
| Scalar fields on anything else (places, notes, repositories). Structural lists are refused. |
| Place type, parent enclosure, name, title, coordinates. The parent must already exist (never minted from a name), cycles are refused, and multi-entry dated enclosures are refused rather than flattened. |
| Edit or remove ONE existing URL entry on a person/place/repository, matched by substring — must match exactly one. The fix for a link filed under the wrong type. |
| Link an existing source to an existing repository, with call number and medium. |
| The same for many sources in one call, each row reported. |
| Attach a named Tag to an object, creating the tag if it doesn't exist. |
| Set or clear the Gramps private flag on an object. |
| Merge duplicates via the server's own merge, inside one transaction. Dry-run by default, and the dry run names what else Gramps would merge — a person merge can merge two families, a family merge two fathers — and what it refuses. For places, settles the survivor's enclosures rather than keeping both places' parents. |
| Remove an event/media/note/tag/child/repository reference, or a place's extra parent; optionally delete if orphaned. A tag can be named; a repository link narrowed to one call number. |
| Permanently delete an object by handle or Gramps ID. Refused when it would strand a note or image only it holds, and for a source that still has citations. |
Read
Tool | Purpose |
| Full detail: name, every alternate name (+ citation counts), gender, events, attributes (+ citation counts), families, media. |
| Relationship, parents, children, event count. |
| Type, date (with its modifier: "between 1882 and 1883", never "1882"), place, description, citation count. |
| Title, author, pubinfo, abbrev — and its real citation count. |
| Name, type, URLs, and the sources it holds. |
| Raw record for any type. Returns the record as stored, which is what an edit needs. |
| Name, title, type, enclosure chain, coordinates, URLs. |
| Page, confidence, date, source — and |
| Type and full text. Notes hold reasoning, so nothing is truncated. |
| Path, mime, checksum, and how many objects reference it. |
| One timeline merging several people or families — a household moving through censuses together. |
| Chronological events for a person or family, each with age, citation count and best confidence, plus an |
| How two people are related, in words and in generation distances. |
| The server's own living/dead verdict, walking relatives, with optional estimated dates and reasoning. |
| Elapsed time between two events — the arithmetic behind every age-at-event check. |
| Record-holders — oldest at death, youngest parent, most children — across the tree, or across one person's ancestors or descendants. An implausible holder is usually a data error. |
| DNA matches with total and largest shared centiMorgans, and any common ancestor identified. |
| Y-DNA haplogroup, broadest clade to terminal. Speaks to the direct paternal line only. |
| Parse pasted shared-segment data into segments and totals. Reports a parse failure as such, rather than as an absence of shared DNA. |
| Researcher details, which travel inside every export. |
| The 25 reports Gramps can generate: Ahnentafel, descendant, family group, kinship, fan and relationship charts, statistics, end-of-line. |
| One report's default options. Reports take a whole option dict, so this is what an override merges over. |
| Generate a report. Living people and private records are left out unless you ask for them. |
| Name substring + optional birth-year range (privacy-filtered). |
| Walk the tree N generations (privacy-filtered). |
| Every tag with handle, name and colour. |
| The type names the write tools accept: Gramps' standard ones and the tree's custom ones. |
Audit
Tool | Purpose |
| Filter any collection server-side with GrampsQL. The workhorse for audits — read the syntax traps below. |
| What references this object — the only correct way to ask "is this source cited?". |
| Candidate duplicates by strategy: |
| Events with no citation, or tagged |
| Person↔family links checked in both directions: a family listed twice, a link one side lacks, a link to nothing. Each finding says how to repair it. |
| Counts of people/families/events/citations/etc. |
| The structured query engine. Indexed columns, |
| The tree's event type vocabulary with the integers it stores. An unexpected name here is usually a typo Gramps accepted as a custom type. |
| Gramps' filter-rule vocabulary — "is a descendant of", "has a common ancestor with" — which neither GrampsQL nor |
| Saved filters on this instance, reusable by name. |
| Save a reusable selection built from those rules. |
| Delete a saved filter. Touches the definition only. |
| Gramps' own genealogical plausibility checks — a mother at nine, a 120-year marriage, an unparseable date. A different audit from the citation sweeps. |
| Read a document image with the engine that suits it: existing OCR text or Tesseract for print, the image itself for an English hand, Transkribus for German and Norwegian handwriting, FamilySearch's index for census tables. A finding aid, not evidence — read the image before citing it. See Reading documents. |
Research tasks — Gramps Web's own task list
Tool | Purpose |
| Add a task to Gramps Web's Tasks view, written as its New Task form writes one: a Source tagged |
| The tasks, in the Tasks view's order, with status, priority, tags, attributes and description; filtered by status, tag or attribute value. A private task is a redacted stub. |
| Set a task's status, priority, privacy or attributes — each replaced, never appended beside the old value, which Gramps Web would not show — or add a paragraph to its description. A private task's description note is private too; making the task public leaves a private note private unless |
A task made here and one made in Gramps Web are the same thing: either shows
in the Tasks view, and in desktop Gramps' To Do gramplet through its note. A
task is a to-do, not evidence, so it is never cited. These are not the
background jobs list_jobs reports.
Ops
Tool | Purpose |
| Full-tree Gramps XML dump to a new file; never replaces one. Run this before any bulk write. |
| Recent writes: what changed, when, by which user. Also how you check whether another session is writing. |
| Undo a transaction, after a conflict check. Dry-run by default. Returns a |
| Recent background jobs for this tree, newest first. |
| One transaction in full, including the objects it changed. Read before undoing. |
| Who added, edited or deleted one record, and when — each change with its transaction. A deleted record by its handle. |
| Whether a background job finished, and whether it worked. Undo, verification and reindex all dispatch to a worker. |
| Rebuild the full-text index behind Gramps Web's search box. The server updates it after each write; rebuild after a large import, or when search misses something the tree holds. |
Reference layer (read-only, never touches the tree)
Tool | Purpose |
| Look a person up in the legacy GEDCOM exports as an untrusted hint. See Reference layer. |
Querying with GrampsQL
query_objects filters in the database rather than pulling a collection and
sifting it in Python. The syntax has traps, verified on every CI run against
gramps-webapi 3.21.1, 3.22.3 and 3.23.1:
Equality is a single
=.page == ""is a parse error.~is substring:description ~ "1871"..lengthworks on any list:media_list.length = 0.A field the object does not have matches nothing, without an error. A zero count can mean a misspelt field.
typeis such a field on events.type = "Birth"matches nothing, silently, on a tree with hundreds of births. Usequery_recordswithevent_typeinstead.A source has no
citation_list— citations point at sources. Useget_backlinksto find uncited sources. Queryingcitation_liston sources matches none, cited or not.Booleans compare as integers:
private = 1, notprivate = true.A list is searched through its items.
~on the list itself asks whether the value is an item, sourls ~ "http"matches nothing.query_objectsrefuses it and names the form that works:.any.(or.all.) reaches the items, andget_<type>follows a handle.Every condition reads the whole collection, object by object, so a GrampsQL read is allowed 120 seconds: an OR of two list conditions took 29 seconds over 6,000 citations.
Useful ones:
citations confidence >= 3 AND page = "" high-confidence claims with no locator
sources media_list.length = 0 documents with no image attached
media desc = "" media nothing identifies
person urls.any.path ~ "findagrave" a URL anywhere among a person's links
citation note_list.any.get_note.text.string ~ "x" a citation whose note mentions xEvery tool has an LLM-facing docstring explaining when to use it, parameter semantics, and good citation practice.
Reading documents: ocr_media
Gramps Web's own OCR is Tesseract, which reads print. The documents a family
history turns on are handwritten — wills, deeds, letters, German church books
in Kurrent, Norwegian parish registers — and which reader to trust depends on
the hand and the language. ocr_media routes by what the caller says the
document is (doc_type) and its language (lang):
Document | Read by | Why |
| Text the media already carries — a Transcript note, a PDF's text layer — or the Library of Congress's or the Internet Archive's OCR for a page a URL in its sources names; otherwise Tesseract, through Gramps Web | Existing text costs nothing; Tesseract reads print well |
| The image, returned as MCP image content with a diplomatic transcription instruction for the calling model: spelling as written, line breaks and abbreviations kept, | Vision models read 18th- and 19th-century English hands at 5.7–7 % character error, better than dedicated engines (Humphries et al.) |
| Transkribus only (its German Kurrent model). Without Transkribus, refused — never a vision read, even when asked, and not for a German table or volume either | Vision models measure 48.8 % character error on historical German (METATR, May 2026, READ-2016) |
| Transkribus (NorHand 1820–1940), with the image to check it; without Transkribus, the image and a warning of its error rate | Vision models measure about 10 % on Norwegian (METATR, NorHand) |
| Transkribus' general model (Text Titan II), with the image; without it, the image and a warning | No benchmark known for a vision read |
| Not read: FamilySearch's index ( | An engine reads across a row and loses which cell belongs to which person |
| FamilySearch Full-Text Search first, then Transkribus page by page | Searching a volume beats reading it |
engine asks for one reader outright: existing, tesseract, vision (refused
for German handwriting) or transkribus. A returned image is scaled to what a
current Claude model reads without scaling it again (2576 pixels on the long
edge, 3.75 megapixels); region=[x1, y1, x2, y2], in percent, returns part of
the page at full detail. A PDF is read a page at a time (page): its text
layer, and the scan embedded in the page; a page with no scan is rendered by
Gramps Web, which renders only the first.
Every result carries its provenance — {engine, model, date} — and the same
caveat: the transcript is never the evidence; the citation stays on the
image. store=true keeps a machine reading as a Transcript note on the media
object, headed with its engine, model and date, and Transkribus' PAGE XML in a
second, preformatted note, since Transkribus deletes it a day after the job. A
vision reading is the calling model's to keep, with add_note. A later call
for print finds a stored reading and returns it rather than reading again.
Transkribus is optional, and costs money. API jobs are charged half the
app's rate — about 0.5 credits, roughly €0.12, a page — so a page is sent only
when the call passes spend_credits=true or this month's
GRAMPS_MCP_TRANSKRIBUS_PAGE_BUDGET has room. Each result says what it spent
and how many pages the month has used; the count is kept in
transkribus-usage.json in the cache directory; a page is counted before it is
sent, and not sent if it cannot be counted. A job for the same file, page and
model within a day is fetched again, not paid for again; a failed one is
dropped, so the next call can try afresh. A private media object is never
sent, nor a page in a language no model here covers. Images go to READ-COOP in Austria; its documentation says
an image is used for its job only and not stored, and its terms let it use
submitted material to improve its products.
The Transkribus path is built to the published API and has not yet been exercised live. It follows the Metagrapho v1 OpenAPI document and READ-COOP's documented login, and is tested against fixtures of those shapes; no account was used to build it. The Library of Congress and Internet Archive paths, the image and PDF handling and Tesseract through a task queue were checked against a live tree and the live archives on 2026-10-06.
Reference layer (legacy GEDCOMs)
consult_reference searches the GEDCOM files listed in your TOML config and
returns, per file, matching individuals and their claimed facts. Crucially,
each fact is flagged whether the legacy tree attached a source, with the
source text if present — so the assistant can distinguish "Ancestry cites an
actual death certificate" from "unsourced guess."
Parses GEDCOM 5.5.1, including the Ancestry dialect (
_APID,_TREE, …). Custom underscore tags are carried through, not choked on.Files load lazily and their parsed form is cached on disk (keyed by path/size/mtime), so a big export is parsed once.
Each file has a trust note surfaced in results.
These are untrusted hints. The intended loop: consult a hint → decide which real record to hunt down → create the fact in the tree citing that record — never copy a hint in as if it were sourced.
Your real GEDCOMs are never committed (.gitignore excludes *.ged except the
synthetic test fixture).
Worked example: a person, with a cited birth
You: Add Martha Ellery, female, born 12 Jan 1890 in Columbus, Ohio. I have her birth certificate (Ohio certificate #12345); cite it at very-high confidence.
The assistant calls one tool:
add_person({
"given": "Martha", "surname": "Ellery", "gender": "female",
"birth": {
"type": "Birth",
"date": "12 Jan 1890",
"place": "Columbus, Ohio, USA",
"citation": {
"source_title": "Ohio Birth Certificate #12345",
"page": "certificate no. 12345",
"confidence": "very_high"
}
}
})Behind the scenes the server: creates the Source "Ohio Birth Certificate
#12345" → creates a Citation on it (page + very-high confidence) → finds or
creates the Place "Columbus, Ohio, USA" → creates the Birth Event (dated,
placed, carrying the citation) → creates the Person referencing that event as
their primary birth. It returns the new gramps_id (e.g. I0001).
Ask list_unsourced_facts any time to see what still needs a source. If you
add a fact you can't yet source, pass require_citation=false and it'll be
tagged UNSOURCED for that audit list.
Development
git clone https://github.com/ianderso/gramps-evidence-mcp
cd gramps-evidence-mcp
uv sync --extra dev
uv run pytestThe suite runs against an in-memory fake of gramps-webapi served through
respx, so it needs no Gramps Web instance,
and it fails any test that tries to open a real connection. It exercises the
tool surface the way a client calls it: every tool's schema, the refusal of
unknown arguments, the whole-object write rule across every editing tool, the
privacy filter on each bulk output, and the error envelope every tool returns
instead of raising.
A second suite, tests/live, runs against a real, throwaway gramps-webapi --
3.21.1, 3.22.3 and 3.23.1 in CI, installed from PyPI with no Docker needed. It checks
every server behaviour docs/PITFALLS.md describes, drives
the tools end to end, and runs the same scenarios against the fake and the
server and requires the same answers, so the fake cannot drift from what it
stands in for. CONTRIBUTING.md says how to run it.
CONTRIBUTING.md says what a change is expected to carry.
Limitations
Requires gramps-webapi 3.21 or later (Gramps 6.0); tested on every CI run against 3.21.1, 3.22.3 and 3.23.1. On an older server every tool answers with an
unsupported_servererror naming its version: 3.20 lacks the query endpoints the searches and privacy filter use. Your instance's/api/openapi.jsonis authoritative — check it if a call behaves unexpectedly, and see docs/PITFALLS.md for where the API's behaviour has surprised before.No tree selector. On gramps-webapi the tree is bound to the account you authenticate as; no data endpoint takes a tree parameter. To work against a different tree, use credentials belonging to it.
Type names (
"Birth","Married") are matched against Gramps' English standard names and the tree's custom ones, ignoring case and punctuation, with a few clear synonyms ("Born" is Birth). Anything else is refused with the closest names unlessallow_new_typeasks for a new custom type, so a type name in another language is refused rather than stored: give the English name. See docs/PITFALLS.md section 26.search_people,list_unsourced_factsandfind_duplicatesread whole collections. That suits a personal tree of a few thousand people; they are not built for very large databases.get_factsis computed by the server on every call and takes tens of seconds on a tree of under a thousand people; the call is allowed three minutes.ocr_media's Transkribus path has not been run against the live service. Gramps Web renders only a PDF's first page, so a later page with no scan embedded in it cannot be read as an image.
Repository layout
src/gramps_evidence_mcp/ the MCP server
server.py tool definitions (the 97 tools) and the entry point
service.py genealogy operations; edits go through _mutate()
client.py gramps-webapi REST client
mapping.py Gramps object <-> JSON shapes
models.py Pydantic input models shared by the tools
privacy.py living-person assessment and redaction
gedcom_ref.py read-only reference layer over legacy GEDCOMs
ocr.py ocr_media's routing, images, archives and Transkribus
config.py env vars + TOML
tests/ against an in-memory fake; no live server needed
live/ against a throwaway gramps-webapi (CI: 3.21.1, 3.22.3, 3.23.1)
docker/ docker-compose for a local Gramps Web
docs/
ARCHITECTURE.md how the server is put together
PITFALLS.md gramps-webapi behaviours that have cost real data
ROADMAP.md what is planned (nothing), and what is notdocs/PITFALLS.md is required reading before writing a
script against gramps-webapi directly. Every item in it was learned by losing
something: a keys= PUT that wiped parent links on 65 families, a citation
handle reused across two claims that mis-graded 107 parent-child links, and two
concurrent sessions that took the write path down for ~18 hours.
Available Tools
97 toolsadd_alternate_nameA
Add an alternate (non-primary) name to a person, cited to the record using it.
Use for maiden/married names, aliases, anglicized forms, or nicknames-of-record. The person's primary name is left unchanged. Correct or remove one later with update_alternate_name.
Inputs: person (required), given, surname, citation, +5 more.
add_attributeA
Add a typed key/value attribute to an object. update_attribute changes or removes one already there.
Inputs: name (required), value (required), target (required), object_type (required), +1 more.
add_child_to_familyA
Add an existing person as a child of an existing family.
Links the child both ways: a ChildRef is added to the family and the family is added to the child's parent-family list. The child must already exist. Duplicate children are skipped.
Inputs: child (required), family (required), frel, mrel, +1 more.
add_citationA
Create a standalone Citation on a Source (or reuse an existing one).
Usually you don't call this directly -- pass a CitationInput to add_person / add_event_to_person / add_family instead, which creates the citation and attaches it to the fact in one step. Use this when you want a reusable citation handle to attach to several facts.
Inputs: citation (required).
add_dna_matchA
Record a DNA match as evidence, cited to the test that found it.
Stored as Gramps Web stores a match, so its interface and get_dna_matches read it back. Nothing is written unless the segments parse: unreadable data would otherwise record a match sharing no DNA.
A match proves the two share DNA, not how they are related. Record any relationship separately, with its own evidence.
Inputs: match (required), person (required), citation (required), segments (required).
add_event_refA
Share an existing event with another person, in a role.
For one census entry, residence or burial that several people took part in: each references the same event, so its citations and later corrections serve all of them. Refused if the person already has it; update_event_ref edits that reference. A new fact is add_event_to_person.
Inputs: event (required), person (required), role, attributes, +1 more.
add_event_to_familyA
Add a dated/placed event (fact) to an existing family.
Use for family-level facts: marriage, divorce, residence, census. The event is added with the 'Family' role. Cite it to the record establishing the fact.
Inputs: event (required), family (required), require_citation.
add_event_to_personA
Add a dated/placed event (fact) to an existing person.
Use for facts beyond birth/death: residence, occupation, census, baptism, immigration, marriage-adjacent events, etc. The event's citation should point at the record establishing the fact. Adding a 'Birth'/'Death' event will set the person's primary birth/death reference if not already set.
Inputs: event (required), person (required), require_citation.
add_familyA
Create a family linking parents and children, with an optional cited marriage.
All members must already exist (create them first with add_person). Creating the family automatically links each person's family/parent-family lists, so you don't need to update the individuals separately. Cite the marriage event to a marriage record where possible.
Inputs: father, mother, children, marriage, +3 more.
add_mediaA
Upload a document as a standalone Media object, reusing an identical file already in the tree.
One image, one Media object -- then attach it wherever it belongs with attach_media(media_ref=...) and cite it once per fact it proves. Uploading the same photograph once per person it depicts is the duplicate pattern that later has to be unpicked by hand.
Inputs: file_path (required), description (required), dedup_by_checksum.
add_noteA
Create a research/general note, optionally attached to an object.
Use notes for research logs, reasoning about conflicting evidence, or transcriptions. Attach to a person/event/source by giving target + target_type. Any length is written in one call; a server error says whether anything was.
Inputs: text (required), target, note_type, target_type, +1 more.
add_personA
Create a new person, optionally with cited birth and/or death events.
Use this to add someone to the tree. Good practice: attach the birth event with a citation to the specific record that proves it (a birth or baptism certificate, a census entry), setting the citation's confidence honestly. If you only have a legacy-tree hint with no underlying record, either omit the event or set require_citation=False so it's flagged for follow-up.
Returns the new person's handle and gramps_id.
Inputs: given (required), birth, death, gender, +4 more.
add_placeA
Create a place deliberately, with a type and a parent.
Use this instead of letting a place appear as a side effect of naming one in an event. That route produces an untyped, unparented place whose title is the bare string you typed, which is how duplicate hierarchies start.
Inputs: name (required), code, title, parent, +4 more.
add_repositoryB
Create a Repository (an institution or place that holds sources).
Repositories sit at the top of the evidence model: Repository -> Source -> Citation -> fact. Create these for archives, libraries, cemeteries, or websites you'll cite sources from.
Inputs: name (required), url, allow_new_type, repository_type.
add_research_taskA
Add a research task to Gramps Web's own task list (its Tasks view).
Written exactly as Gramps Web's New Task form writes one: a Source tagged ToDo, Status 'Open', a Priority, and the description as a To Do note. A task is a to-do, never evidence: never cite it.
Inputs: title (required), tags, private, priority, +3 more.
add_sourceA
Create a Source (a body of evidence: a record set, book, certificate, website).
In the Gramps evidence model a Source is what you cite through a Citation. Create the source once, then create citations against it for each fact it supports. Optionally link it to a Repository (where the source is held).
Inputs: title (required), author, media_type, repository, +4 more.
add_urlA
Add a web URL to a person, place, or repository.
Only these three object types carry a URL list. For sources/citations, record a web address as an attribute (add_attribute) instead.
Inputs: url (required), target (required), object_type (required), url_type, +2 more.
assess_livingARead-only
Ask the server whether a person is probably still alive, and why.
The server walks relatives to decide, so it handles people with no dates
of their own — someone undated whose children died a century ago. Use it
before publishing or sharing anything, and use explain when you want to
see the reasoning rather than just the verdict.
Note this is advisory. Bulk output is filtered by this server's own rule regardless of what this returns.
Inputs: person (required), explain, average_generation_gap, max_age_probably_alive.
attach_mediaA
Attach an image or document to an object -- a new upload, or one already in the tree.
With file_path, the bytes are uploaded into the tree's managed media directory (so don't point at files you don't want copied); if that exact file is already present it is reused rather than duplicated. With media_ref, an existing Media object is linked.
One image should be ONE Media object, linked from each object it belongs to
and cited once per fact it proves. Uploading the same photograph separately
for the husband and the wife creates duplicates that have to be unpicked
later. The write is verified afterwards: verified: false means nothing
attached, whatever the rest of the result says.
Inputs: target (required), file_path, media_ref, description, +1 more.
check_family_linksARead-only
Audit the links between people and families, in both directions.
Finds a family a person lists twice, a child a family lists twice, a link one side holds and the other lacks, and links to objects that do not exist. Each finding says how to repair it. Reports only.
Inputs: limit, include_private.
cite_child_linkA
Cite the parent-child link itself, on the family's ChildRef.
"This child belongs to these parents" is a DIFFERENT claim from "this child appears in this record", and it needs its own citation object. Reusing the child's existing citation handle makes the link inherit a confidence that was assigned to another claim entirely, so the link displays a confidence its evidence never earned.
Note what a ChildRef citation asserts: BOTH sides of the link. A census naming only the mother does not document the father. Where only one parent is evidenced, cite that parent's relationship instead of implying both.
Inputs: child (required), family (required), citation (required).
cite_eventA
Attach a citation to an event that ALREADY exists.
Use this to source an event you didn't create with an inline citation -- e.g. one added through the Gramps web UI, or an unsourced event surfaced by list_unsourced_facts. The citation is resolved/created (existing handle/id, or an inline source_title + page + confidence) and appended to the event's citation list (no duplicates). Does not change any other event field.
Inputs: event (required), citation (required).
cite_objectA
Attach a citation to any object that carries one -- not just events.
cite_event covers facts; this covers the rest. The important case is the FAMILY, whose citation supports a claim no event makes: that these two people were a couple. Person-level citations are for evidence about the individual as a whole (an identity document) rather than about one dated fact -- prefer citing the specific event where one exists.
A NAME is cited with object_type='name': "this record gives this spelling" is narrower than evidence about the person. To cite a parent-child link, use cite_child_link: that is a different claim and needs its own citation.
Inputs: ref (required), citation (required), object_type (required), name.
consolidated_timelineARead-only
Merge several people or families into one chronological timeline.
The way to see a household move together through censuses, or to check whether a family's events are mutually consistent. Carries the same citation count and confidence per event as get_timeline, with an uncited_count across the whole set.
Inputs: targets (required), limit, anchor, event_types, +2 more.
consult_referenceARead-only
Consult the legacy GEDCOM reference layer for HINTS (never authoritative).
Searches the configured Ancestry/FamilySearch exports and returns, per file, matching individuals and their claimed facts. Crucially, each fact is flagged whether the legacy tree attached a source, with the source text if present -- so you can distinguish 'they cite an actual death certificate' from 'unsourced guess'. These are UNTRUSTED hints: use them to decide what real record to hunt for, then create the fact in the tree citing that record -- do not copy a hint in as a sourced fact. Probably-living people are withheld and counted.
Inputs: name (required), year_tolerance, include_private, approx_birth_year.
create_filterA
Save a reusable custom filter built from Gramps' own rules.
Worth doing for a selection you will run repeatedly — an audit scope, a branch of the tree — because the filter then has a name rather than being retyped each time.
Inputs: name (required), rules (required), namespace (required), invert, +2 more.
db_statsARead-only
Counts of people, families, events, citations, sources, repositories, places, media, and notes in the tree. A quick health/overview check.
Inputs: no named parameters.
delete_filterADestructive
Delete a saved custom filter.
Deletes the filter definition only. Nothing in the tree is touched.
Inputs: name (required), namespace (required).
delete_objectADestructive
Permanently delete an object from the tree by handle or gramps_id.
DESTRUCTIVE. The server also removes every reference to it. Refused when it is the only holder of a note or image (pass carry_to to move them), and for a source with citations, which the server would delete with it.
Inputs: target (required), object_type (required), carry_to.
detach_objectADestructive
Remove a reference from an object: an event from a person, an image from a source, a tag, a note, a child from a family.
The reference is removed; the object itself survives unless delete_if_orphan is set AND nothing else points at it -- which is checked, because deleting something other facts still reference leaves dangling handles behind.
Inputs: child (required), parent (required), child_kind (required), parent_type (required), +2 more.
event_spanARead-only
Measure the elapsed time between two events.
The arithmetic behind most plausibility checks: age at marriage, years between a census and a death, how long a widow waited. Doing this by hand from two formatted date strings is where transcription errors hide.
Inputs: event1 (required), event2 (required), as_age, precision.
export_backupA
Write a full-tree export to disk. Take one before any bulk write.
Cheap insurance: a few seconds and one file. A bulk write that goes wrong cannot always be undone transaction by transaction; a dump can be re-imported.
Inputs: dest_path, export_format.
find_duplicatesARead-only
Find likely-duplicate objects. Reports only -- it never merges anything.
Duplicates are not merely untidy: a duplicate SOURCE makes a single-sourced fact look corroborated, which is a false evidentiary claim. But the reverse error is just as real -- an index entry and the register page it indexes are TWO documents and must stay separate. This tool finds candidates; deciding which are truly the same document is yours. Merge with merge_objects.
Inputs: kind (required), limit, include_private.
get_ancestorsARead-only
Walk a person's ancestors up to N generations (a nested parents tree).
Living/private ancestors appear as redacted stubs unless include_private is set.
Inputs: person (required), generations, include_private.
get_backlinksARead-only
List everything that references this object, grouped by type.
The right way to ask "is this source actually cited?", "which facts rest on this citation?", or "is it safe to delete this?" -- an object with zero backlinks is orphaned; one with backlinks will leave dangling references if deleted.
Inputs: ref (required), object_type (required).
get_citationARead-only
Read one citation: page, confidence, date, and its source.
cited_by_count is the useful part. Zero means nothing references this
citation — it is orphan debris, and uncite should have deleted it.
Inputs: citation (required).
get_descendantsARead-only
Walk a person's descendants up to N generations (a nested children tree).
Living/private descendants appear as redacted stubs unless include_private is set.
Inputs: person (required), generations, include_private.
get_dna_matchesARead-only
List the DNA matches recorded against a person.
Each match reports total shared centiMorgans and the largest single segment — the two figures a relationship estimate actually rests on — plus any common ancestor already identified.
DNA evidence works differently from documentary evidence. A match
proves a biological relationship exists; it does not say which one. Shared
cM constrains the possibilities and rarely resolves them, and it says
nothing about the paper trail. unattributed_count is the useful number:
matches with no common ancestor identified are the open research.
Inputs: person (required), include_raw.
get_eventARead-only
Get an event: type, date, place handle, description, citation count, and attributes. Use to inspect an event before citing or editing it.
Inputs: event (required).
get_factsARead-only
Read the tree's record-holders: oldest at death, youngest parent, most children.
Superlatives across a set of people, not statistics about one person. An implausible holder — a father at eight, a death at 130 — is usually a data error, which makes this a quick plausibility check. Living and private people are excluded unless include_private is set. Slow: the server computes it all per call.
Inputs: rank, person, person_filter, include_private.
get_familyARead-only
Get a family: relationship type, parent handles, child handles, event count.
Inputs: family (required).
get_jobARead-only
Check whether a background job has finished, and whether it worked.
Undo, verification, import and reindex are dispatched to a worker and
answer before the work is done. Poll this until finished is true, then
read succeeded. Submitting one of those operations and never checking
leaves you assuming an outcome you have not seen.
Inputs: task_id (required).
get_mediaARead-only
Read one media object: path, mime type, checksum, description, date.
referenced_by_count above one is usually correct — one image cited from
every fact it proves. Several media objects sharing a checksum is the
duplicate that find_duplicates(kind='media_checksum') hunts.
Inputs: media (required).
get_noteARead-only
Read one note in full, with its type and what it is attached to.
Notes hold the researcher's own reasoning — why a conflict was resolved one way, what a hard-to-read page actually said — so the text comes back whole rather than truncated.
Inputs: note (required).
get_objectARead-only
Read any object's raw record -- including places, media, notes and citations, which have no shaped getter.
Returns the record as stored, which is what you want before editing one. For people and sources the shaped getters (get_person, get_source) are easier to read.
Inputs: ref (required), object_type (required), keys.
get_personARead-only
Get full detail for one person: name, gender, events (with citation counts), attributes, family links, and media count.
Direct lookup is allowed even for living/private individuals (this is your own local tool); only bulk/list tools filter them. Use this to inspect someone before adding facts, or to check whether an event is already cited.
Inputs: person (required).
get_placeARead-only
Read one place: name, title, type, enclosure, coordinates and URLs.
The enclosed_by handles are the jurisdictional chain. A place with none
is orphaned in the hierarchy, which is usually a place that got minted
from an event's place string rather than created deliberately.
Inputs: place (required).
get_record_historyARead-only
Who added, edited or deleted one record, and when, newest first.
Each change names its transaction: get_transaction shows what it changed. Before gramps-webapi 3.22 the whole transaction log is read instead: slower, and the result says so.
Inputs: ref (required), object_type (required), field, limit.
get_relationshipARead-only
Work out how two people in the tree are related.
Returns the relationship in words plus the generation distance from each
person to their common ancestor. related is false when no common
ancestor was found within the search depth — which is a finding in itself
if you expected one.
Inputs: person1 (required), person2 (required), depth, all_paths.
get_report_optionsARead-only
Read one report's default options before running it.
Reports take a full option dict, not a partial one, so the way to change a single setting is to read these defaults and override that key.
Inputs: report_id (required).
get_repositoryBRead-only
Get a repository: name, type, URLs, address count, and (if available) the number of sources it holds.
Inputs: repository (required).
get_researcherARead-only
Read the researcher details recorded for this tree.
These are embedded in every export, so they travel with any GEDCOM or Gramps XML you hand to someone else. Worth checking before sharing one.
Inputs: no named parameters.
get_sourceARead-only
Get a source: title, author, publication info, abbreviation, linked repositories, media/note counts, attributes, and citation count.
Use to inspect a source before citing through it or editing it.
Inputs: source (required).
get_timelineARead-only
Build a chronological timeline of someone's life events.
Each entry carries their age at the time, how many citations support the
event, and the strongest confidence among them — so a timeline doubles as
a readable audit of where the evidence thins out. uncited_count says how
many events on it rest on nothing.
Inputs: target (required), limit, ancestors, offspring, +2 more.
get_transactionARead-only
Read one transaction in full, including the objects it changed.
list_transactions summarises; this shows what actually moved. Read it before undoing anything.
Inputs: transaction_id (required).
get_ydnaARead-only
Report a person's Y-DNA haplogroup, from broadest clade to terminal.
Y-DNA follows the direct paternal line only, so it speaks to one thread of a tree and is silent on every other. A shared terminal clade indicates a common paternal ancestor, usually far further back than any record reaches — it corroborates a surname line rather than proving a named link.
has_data is false when the person has no Y-DNA recorded, which is the
normal case.
Inputs: person (required), include_raw.
link_repositoriesA
Link many sources to their repositories in one call -- a sweep.
Each row is link_repository: its own write and transaction, so a failed row does not stop the rest. Each row reports linked, already_linked, missing or error.
Inputs: items (required), allow_new_type.
link_repositoryA
Link an existing source to an existing repository that holds it.
Adds a repository reference (with an optional call number) to the source. Both objects must already exist. Duplicate links to the same repository are skipped.
Inputs: source (required), repository (required), media_type, call_number, +1 more.
list_custom_filtersARead-only
List the custom filters already saved on this instance.
A saved filter can be reused by name from query_objects and the
timelines, so a complicated selection is defined once.
Inputs: namespace.
list_event_typesARead-only
List the event type names this tree uses, with their stored integers.
Useful before a query_records filter, and as a check on itself: an
unexpected type name in the list is usually a typo that Gramps silently
accepted as a new custom type.
Inputs: no named parameters.
list_filter_rulesARead-only
List the filter rules Gramps offers in a namespace.
This is the vocabulary that query_records and GrampsQL cannot reach:
"is a descendant of", "has a common ancestor with", "matches another
filter". Read it before building a custom filter with create_filter.
Inputs: namespace.
list_jobsARead-only
List recent background jobs for this tree, newest first: undo, verify, reindex, report.
Use when you have lost a task_id, or to see whether anything is still running before starting a write session. Research tasks are list_research_tasks.
Inputs: limit.
list_object_typesARead-only
The tree's type vocabularies: Gramps' standard names and its own custom ones.
The write tools take any of these, in any case. A name in neither is refused unless allow_new_type asks for a NEW custom type, which then stays in the tree's vocabulary for good.
Inputs: no named parameters.
list_reportsARead-only
List the reports this Gramps instance can generate.
Gramps ships a full report engine — Ahnentafel, descendant reports, family group sheets, kinship, fan and relationship charts, statistics, and an end-of-line report that lists exactly where research stops. Each entry names the option keys it accepts; read the defaults with get_report_options before overriding any.
Inputs: no named parameters.
list_research_tasksARead-only
List the research tasks in Gramps Web's Tasks view: title, status, priority, tags, attributes and description.
Ordered as the view orders them: Open, In Progress, Blocked, Done. Not background jobs (list_jobs). A private task is a redacted stub.
Inputs: tag, status, attributes, include_private.
list_tagsARead-only
List all tags in the tree with their handle, name, and color.
Use to see what labels already exist before tagging (tag_object matches by exact name).
Inputs: no named parameters.
list_transactionsARead-only
Recent writes to the tree: what changed, when, by which user.
Each entry's transaction_id is what undo_transaction takes. Useful for "what did that bulk pass actually do?" and for finding the transaction to reverse when it did the wrong thing.
Inputs: limit.
list_unsourced_factsARead-only
Audit: list events that lack a citation or are tagged UNSOURCED.
This is the core quality query for a fully-cited tree -- run it to find facts that still need a source. Returns each offending event with its person, type, date, and the reason ('no-citation' or 'tagged-unsourced').
Inputs: person, include_private.
merge_objectsADestructive
Merge two objects that are the same thing. Dry-run by default.
This uses Gramps' own server-side merge: every reference to drop is
re-pointed at keep and the subordinate lists are unioned, in one
transaction. Do NOT do this by hand: a manual merge that misses one of the
lists the dropped object carries loses what was on it.
Before merging, be sure they really are one thing. Two records OF the same event are two documents: an index entry and the register page it indexes stay separate, and merging them would turn two independent citations into one, silently weakening every fact that rested on both. Conversely, the same census page entered once per household member IS one document, and leaving the duplicates makes single-sourced facts look corroborated.
Reversible: the merge is one transaction, so list_transactions + undo_transaction can back it out.
Inputs: drop (required), keep (required), object_type (required), dry_run, +1 more.
move_childADestructiveIdempotent
Move a child to another family with the link's citations and notes, placed in birth order. Detaching and re-adding loses both. The new family is written first: a failure leaves the child in both, never in neither.
Inputs: child (required), to_family (required), from_family (required), frel, +2 more.
ocr_mediaA
Read a document image with the reader that suits it; doc_type and lang choose. print: text the media or its sources already carry, else Tesseract. hand: an English hand returns the image for you to transcribe as the result instructs; German goes to Transkribus only, never a vision read; Norwegian and others to Transkribus, with the image to check. table: FamilySearch's index (get_records_on_image). volume: full-text search first.
Transkribus costs credits: within the monthly budget, or with spend_credits. The transcript is never the evidence; the citation stays on the image.
Inputs: media (required), lang, page, store, +5 more.
parse_dna_segmentsARead-only
Parse pasted shared-segment data into structured segments and totals.
Use this to check what a match file actually contains before recording it. Returns each segment plus the total and largest-segment centiMorgans.
If nothing parses, parsed is false and the reason is given. That
distinction matters: the server answers unreadable input with zero
segments and a success status, which would otherwise read as "this person
shares no DNA" rather than "I could not read that".
Inputs: data (required).
query_objectsARead-only
Query any collection with a server-side filter. The workhorse for audits.
Use this instead of fetching a collection and filtering it yourself: whole-collection pulls are slow on any real tree, and the filter runs in the database. Typical audit questions it answers directly:
uncited high-confidence claims: citations where
confidence >= 3 AND page = ""documents with no image: sources where
media_list.length = 0anonymous media: media where
desc = ""a URL anywhere:
urls.any.path ~ "blm.gov"; a list is searched through its items, never with~on the list itself
To ask "what cites this source?" use get_backlinks -- a source has no citation_list, because citations point at IT, and reading citation_list on a source reports zero for every source in the tree.
Private records and living people come back as redacted stubs.
Inputs: object_type (required), gql, keys, page, +5 more.
query_recordsARead-only
Query any collection server-side, with columns, filters and sorting.
More capable than query_objects and the tool to reach for on an audit.
It reads indexed columns, reaches arbitrary paths inside the stored object,
and follows relationships — so "families where the mother died before the
father" or "events whose place is in Ohio" are single queries.
It is the only way to filter events by type. GrampsQL cannot: the word
is shadowed, so type = "Birth" silently matches nothing. Pass event_type
here instead.
Returns rows plus a total count and a next_after cursor. Private records,
living people and families with a living parent come back as redacted stubs.
Inputs: object_type (required), after, limit, where, +5 more.
reindex_searchAIdempotent
Rebuild the full-text index behind Gramps Web's search box.
The server updates the index in the background after each write. Rebuild it after a large import, or when search misses something the tree holds. Returns a task_id to poll with get_job.
Inputs: full.
run_reportAIdempotent
Generate a report and return the file it produced.
Living people and private records are left out (living_people 0, incl_private false) unless you pass those options or include_private; Gramps' own default includes both.
Usually runs in the background, returning a task_id to poll with get_job.
Inputs: report_id (required), locale, options, include_private.
search_peopleARead-only
Search people by name substring and optional birth-year range.
Bulk output: probably-living people (born < 110 years ago with no recorded death) and records marked private are returned as redacted stubs (ids only) unless include_private is set. Fetch a specific person by id with get_person if you need their detail.
Inputs: name (required), birth_year_max, birth_year_min, include_private.
set_family_parentADestructiveIdempotent
Set, replace or remove the father or mother of an existing family -- the wife a record names -- keeping both sides of every link, children and all.
Inputs: role (required), family (required), person, replace.
set_privateADestructiveIdempotent
Set (or clear) the Gramps private flag on an object.
Private records are withheld from bulk output (queries, searches, tree walks, timelines, reports) unless a call passes include_private.
Inputs: target (required), object_type (required), private.
tag_objectA
Attach a named Tag to an object, creating the Tag if it doesn't exist yet.
Tags are lightweight cross-cutting labels ('Verified', 'Needs review', 'DNA-confirmed'). Matching is by exact name; an existing tag is reused.
Inputs: tag (required), target (required), object_type (required), color.
unciteADestructive
Detach a citation from an object, deleting it if it is left orphaned.
Detaching without deleting is how orphan citations accumulate: the fact the citation supported is gone, but the citation sits in the database still looking like evidence of something. Use this when a citation was attached to the wrong fact, or when a superseded bucket citation is replaced by the real record.
Inputs: ref (required), citation (required), object_type (required), name, +2 more.
undo_transactionADestructive
Undo a past transaction, after checking whether it can be undone cleanly.
A conflict means an object was edited again after this transaction; undoing anyway throws that later edit away. The conflict check is free and runs first, so the default tells you what you are dealing with before anything changes.
Inputs: transaction_id (required), force, dry_run.
update_alternate_nameADestructiveIdempotent
Correct, retype or remove one alternate name, in place.
Edited in place, the name keeps its citations. The primary name is update_person(name=...); a new name is add_alternate_name.
Inputs: match (required), person (required), given, remove, +6 more.
update_attributeADestructiveIdempotent
Set or remove ONE attribute on an object in place, keeping its citations, notes and privacy.
Inputs: name (required), target (required), object_type (required), match, +2 more.
update_child_refADestructiveIdempotent
Change a child's relationship to the father or mother -- a stepson held as a birth child -- in place.
The child keeps the link's citations, notes and its place in the birth order, which detaching and re-adding the child loses.
Inputs: child (required), family (required), frel, mrel, +1 more.
update_citationADestructiveIdempotent
Edit a citation's locator, confidence, date, or the source it points at.
A page-less citation on a long document is not a locator, and a confidence is a per-instance judgement, not a property of the source class.
IMPORTANT: a citation carries ONE confidence, and it belongs to ONE claim. Every fact attached to this citation shares whatever you set here. If the same page supports a second, different claim (a census page proving both "this child appears here" and "these are her parents"), make a SECOND citation on the same source and page -- do not re-grade this one.
Inputs: citation (required), date, page, source, +1 more.
update_citationsADestructiveIdempotent
Re-write many citations' pages or confidences in one call -- a sweep.
Each row is update_citation: its own write and transaction. A row whose live page no longer starts with expect_page_prefix is reported as drifted, not overwritten. Rows report applied, unchanged, drifted, missing or error.
Inputs: items (required).
update_eventADestructiveIdempotent
Edit an existing event in place: its type, date, place or description.
Only what you pass changes. The event keeps its id, citations, notes, media and every person sharing it, so correct a wrong type or an unsupported place here rather than replacing the event. Citations: cite_event.
Inputs: event (required), date, place, clear_date, +4 more.
update_event_refBDestructiveIdempotent
Change a person's reference to a shared event in place: its role, or attributes such as 'As enumerated' or 'Age'.
Inputs: event (required), person (required), role, attributes, +1 more.
update_mediaBDestructiveIdempotent
Edit a media object's description, date, or path.
Inputs: media (required), date, path, description.
update_object_fieldsADestructiveIdempotent
Set scalar fields on any object -- the escape hatch for places, notes, repositories and the rest.
Only scalar fields are settable. Structural lists (citation_list, event_ref_list, media_list, ...) are refused on purpose: replacing one wholesale is exactly how references get silently dropped. Each has its own tool -- cite_object, detach_object, tag_object, attach_media.
Inputs: ref (required), fields (required), object_type (required), allow_new_type.
update_personADestructiveIdempotent
Edit a person's gender, primary name, or privacy flag.
Replacing the primary name preserves the old one as an 'Also Known As': a name in the tree came from some record, and dropping it loses the link to whatever document used it. To add a name without replacing the primary one, use add_alternate_name.
Inputs: person (required), name, gender, reason, +2 more.
update_placeADestructiveIdempotent
Edit a place's type, parent enclosure, name, title, or coordinates.
This is the tool update_object_fields deliberately refuses to be: place_type and the enclosure are structural, so they get guard rails here -- the parent must already exist, setting it cannot create an enclosure cycle, and a place holding several dated enclosures (a territory-to-state succession) is refused rather than silently flattened. Aim for every place typed, every non-country place parented, and street addresses on events rather than in the place tree.
Inputs: place (required), code, name, title, +6 more.
update_research_taskADestructiveIdempotent
Change a research task's status, priority, privacy or attributes, or add to its description.
Each attribute is replaced, never added beside the old one, so Gramps Web shows the value set. add_attribute would append a second Status that Gramps Web ignores. A private task's description note is private too; making the task public leaves a private note private unless private_note=false. Refuses a source that is not a task.
Inputs: task (required), status, private, priority, +4 more.
update_sourceADestructiveIdempotent
Edit an existing source's title, author, publication info, and/or abbreviation.
Only the fields you provide are changed; the rest are left as-is.
Inputs: source (required), title, author, abbreviation, +1 more.
update_urlADestructiveIdempotent
Edit or remove ONE existing URL entry on a person, place, or repository.
add_url can only append -- this corrects an entry already there (the classic case: a Find a Grave link filed under the wrong type). The other fields on the object are untouched.
Inputs: match (required), target (required), object_type (required), url, +4 more.
verify_treeARead-only
Run Gramps' own genealogical plausibility checks over the whole tree.
This is a different audit from the citation ones. list_unsourced_facts
asks whether a claim has evidence; this asks whether a claim is possible —
a mother bearing a child at nine, a marriage lasting 120 years, a date that
will not parse. A wrong date can be impeccably sourced, so these catch what
a citation sweep cannot.
Leave the thresholds alone on a first run; the server's defaults are the conventional ones. Tighten a specific bound when chasing a specific class of error.
May run in the background, in which case a task_id comes back — poll it with get_job.
Inputs: tree_id, max_spouses, estimate_age, max_father_age, +13 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 97 tools
Several tool families overlap heavily: query_objects vs query_records, get_object vs the shaped getters (get_person/get_source/get_citation), and the citation family (add_citation, cite_event, cite_object, cite_child_link) all target related operations. The very detailed descriptions do draw boundaries, but with 97 tools an agent can easily misselect among near-synonyms.
The dominant pattern is consistent snake_case verb_noun (get_person, add_family, update_citation, list_jobs, delete_object). A few outliers (uncite, consolidated_timeline, event_span, db_stats) break the pattern, but overall it is predictable.
97 tools is an extreme mismatch for any single server, well into the 50+ range the rubric flags. Even a complex genealogy/evidence domain does not justify this surface, and the count itself creates discovery and selection burden.
Coverage is unusually broad: CRUD on people/families/events/sources/citations/repositories/media/places/notes, plus citations, DNA, reports, filters, transactions, jobs, audits and timelines. Minor gaps exist (no explicit update/delete for repositories, notes, or tags), but generic tools like update_object_fields and delete_object cover most of them.
Maintenance
Related MCP Connectors
Family history and genealogy in your AI: your tree and FamilySearch, with a source for every fact.
Read-only A2Me family context tools for AI assistants (members, dates, activity, relationships)
Self-curating memory your AI tools share: provenance, verified citations, and trust tiers.
Governed, auditable knowledge your team curates for its AI assistants, self-hostable
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to create, edit, and query genealogical data from GEDCOM files. Supports complex genealogy searches, automatic data enrichment from web sources, relationship analysis, and biography generation for individuals and families.17MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to interact with Gramps genealogy databases for intelligent family tree research and management. Provides comprehensive tools for searching family data, creating records, analyzing relationships, and tracking genealogy research through natural language.51AGPL 3.0
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to query genealogy data from GEDCOM files with 24 tools for search, navigation, relationship analysis, and timeline events.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI assistants to query, validate, and safely update GEDCOM family-history files through a local MCP server with read-only tools and reviewable changesets.1MIT