
Gramps Evidence
io.github.iandersoPublished by GitHub account iandersov2.3.0Updated Oct 25, 2026
Read/write a self-hosted Gramps Web family tree where every fact carries a citation.
Overview
MCP server giving an AI assistant read/write access to a self-hosted Gramps Web family tree, with citations required on every recorded fact.
- What you can do
- Create and edit people, families, events, places, notes, media, sources, citations and repositories in a Gramps Web tree, with citations attached to facts and parent-child links cited separately. Attach, move and remove citations, and audit the tree for unsourced facts, duplicates, backlinks and link problems. Search and query records with GrampsQL, build timelines, run Gramps reports, and export backups. Consult legacy GEDCOM exports as a read-only reference layer of untrusted hints. Optionally OCR media, including handwriting routes through Transkribus.
- When to use it
- For genealogists running their own Gramps Web instance who want an assistant to enter, cite, correct and audit family-tree data while keeping evidence attached to every claim. Also useful for reviewing legacy Ancestry or FamilySearch exports as hints rather than sources.
- Requirements
- A running Gramps Web instance with gramps-webapi 3.21 or later, reachable over HTTP. A dedicated Gramps Web user with at least the editor role, supplied via GRAMPS_MCP_USERNAME and GRAMPS_MCP_PASSWORD, plus GRAMPS_MCP_API_URL. The uv runtime to launch the PyPI package over stdio. An absolute path to a TOML config via GRAMPS_MCP_CONFIG for reference GEDCOMs and the privacy flag. Optional Transkribus account credentials for OCR.
Installation
In SourceWeft
- Open Gramps Evidence in the dashboard and add it to a workspace.
- Enable the server for the chats that should use its tools.
Desktop only via STDIO. STDIO servers start a local process, so they need the SourceWeft desktop host.
Other MCP clients
Follow the launch instructions in the repository.
README
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
- How it connects
- Setup
- Configuration
- Client configuration
- Remote access (Claude web / mobile)
- The evidence model & transactions
- Privacy
- Tool reference
- Reading documents:
ocr_media - Reference layer (legacy GEDCOMs)
- Worked example
- Development
- Limitations
- Repository layout
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:
(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.
Then create the first owner (browser wizard at http://localhost:5555, or CLI) and the dedicated MCP editor user:
The tree named by GRAMPSWEB_TREE (default MyTree) is created on first start.
Install the MCP server
That 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.
TOML file (see gramps_mcp.example.toml)
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/):
Claude Code:
Any 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
That 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:
Add it to your Gramps Web compose (same network), pointing at the API by service name and reading secrets from the environment:
3. 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:
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:
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
Cite — attaching evidence to a claim
Edit — correcting what is already there
Read
Audit
Research tasks — Gramps Web's own task list
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
Reference layer (read-only, never touches the tree)
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:
Every 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):
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:
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
The 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
docs/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.
Source: README.md at commit 77aceea
Tools
0Version history
7- v2.3.0LatestOct 10, 2026
- v2.2.1Oct 9, 2026
- v2.2.0Oct 6, 2026
- v2.0.0Oct 6, 2026
- v1.2.0Oct 5, 2026
- v1.1.0Oct 1, 2026
- v1.0.1Sep 30, 2026
Score
50% of the applicable checks met
How complete this listing is, computed from the registry entry, the endpoint and public sources. Not a rating of the server and not a statement that it is safe or working today: reachability is a dated check, and no installs or reviews go into the score.
- Reached in the last 30 days25 pts
Keep the endpoint answering the MCP handshake; SourceWeft probes it every 30 days.
- Auth declared10 pts
Declare the credential headers in the registry entry, or keep the endpoint answering the probe so its auth can be classified.
- Tools listed15 pts
Answer tools/list without a credential, or ship a package that starts in a sandbox without one.
- DescriptionRequired15 pts
- Logo10 pts
- Documentation15 pts
- License10 pts

