Skip to main content
Glama

ftb-mcp

CI Python License

A read-only MCP server exposing genealogy data from a MyHeritage Family Tree Builder (.ftb) file or a GEDCOM (.ged) export over HTTP.

.ftb files are plain SQLite databases, but the format is undocumented. This repository contains both the server and — in Format notes — the results of reverse engineering the schema against a real 1864-person tree.

The FTB schema is also the server's internal representation: a GEDCOM file is imported into an in-memory database of that shape, so all 17 tools work identically against either source. See GEDCOM support.

Install

python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"

Related MCP server: mcp-kindle

Run

ftb-mcp --db-path kafkova.ftb                      # http://127.0.0.1:8000/mcp
ftb-mcp --gedcom-path kafkova.ged                  # a GEDCOM export instead
ftb-mcp --db-path kafkova.ged                      # .ged/.gedcom is detected too
ftb-mcp --db-path kafkova.ftb --host 0.0.0.0 --port 9000 --path /mcp
ftb-mcp --db-path kafkova.ftb --transport stdio    # for stdio-based clients

Option

Env var

Default

--db-path

FTB_DB_PATH

--gedcom-path

FTB_GEDCOM_PATH

--host

FTB_HOST

127.0.0.1

--port

FTB_PORT

8000

--language

FTB_LANGUAGE

the file's own project language

--path

/mcp

--transport

streamable-http

Exactly one of --db-path / --gedcom-path is required.

An .ftb file is opened with SQLite's mode=ro URI; a GEDCOM file is read once into memory. No tool writes, and there is no code path that can modify either file.

Registering with Claude Code

claude mcp add --transport http ftb http://127.0.0.1:8000/mcp

Tools

Discovery

Tool

Purpose

get_tree_info

Tree name, source application, languages, entity counts, year span

search_persons

Search by name, gender, life dates, living status; paginated

list_surnames

Surname frequency with birth-year span

search_places

Places ranked by event count

search_notes

Full-text search over research notes

Person detail

Tool

Purpose

get_person

Full profile; include selects facts/families/notes/citations/media

get_person_facts

Life events, filterable by GEDCOM tag

get_person_timeline

Own events merged with marriages and children's births

Relations

Tool

Purpose

get_relatives

Parents, siblings, spouses, children with relationship types

get_ancestors

Pedigree with Ahnentafel numbering

get_descendants

Descendant tree with per-generation counts

find_relationship_path

Shortest kinship path plus a label such as first cousin once removed

get_family

One family: spouses, status, marriage/divorce events, ordered children

Evidence and analysis

Tool

Purpose

get_sources

Archives, record collections, matched trees

get_citations

Citations for a person or from a source

get_media_metadata

Title, description, date, place — text only

get_statistics

Demographics, lifespans, names, places, research completeness

Every text-returning tool accepts an optional language (cs, en, or an FTB language number). Text falls back through requested → Czech → English → any, and results report the language actually used when it differs from the one requested.

Media tools deliberately return no image bytes, no file names and no paths. Scanned-record descriptions often carry genealogical detail found nowhere else, so their text is exposed; the binary content is not.

GEDCOM support

Parsing is done by ged4py. The file is read into an in-memory SQLite database using the FTB schema, so queries.py and graph.py — language ranking, fact shaping, pedigrees, statistics — are shared by both backends rather than duplicated.

What is mapped

GEDCOM

FTB

INDI NAME/GIVN/SURN/NPFX/NSFX/NICK/_MARNM

individual_lang_data columns

NAME with TYPE AKA / MARRIED / BIRTH

aka / married_surname / former_name

SEX

gender

presence of DEAT/BURI/CREM

is_alive 2 (deceased), otherwise 3

BIRT, DEAT, OCCU, CENS, … and EVEN + TYPE

individual_fact_main_data.token / .fact_type

an event's own value, or its ADDR lines

individual_fact_lang_data.header

AGE, CAUS

age, cause_of_death

PLAC strings, deduplicated

places_main_data / places_lang_data

FAM HUSB/WIFE/CHIL, FAMC/FAMS

family_individual_connection

FAMC PEDI Adopted/Foster

role 7 / role 6

MARR present → status 3, DIV → 5

family_main_data.status

SOUR PAGE/QUAY/DATA.TEXT/EVEN.ROLE

citation_main_data + citation_lang_data

SOUR record TITL/AUTH/PUBL/TEXT/_TYPE/_MEDI

source_lang_data

NOTE, on records and on events

note_main_data via token_on_item

OBJE TITL/NOTE

media_item_*FILE is deliberately not imported

HEAD SOUR/GEDC/CHAR/LANG

project_parameters category Header

GEDCOM states each family membership twice — on the family as CHIL/HUSB/WIFE and on the individual as FAMC/FAMS. A file can carry only one side, so both are read and memberships the family record failed to mirror are added afterwards. kafkova.ged does this for two children whose FAMC also records them as adopted and fostered; reading only the family side would drop both relationships.

Two defects in MyHeritage's own export

kafkova.ged is not a conforming GEDCOM file, in two ways worth knowing about because they break other parsers:

1. CONC records split mid-character. Nine values have a multi-byte UTF-8 sequence straddling a line break — Jarmila Mat / 4 CONC \x9bj\xc5\xaf for Matějů. ged4py concatenates CONC values as bytes and decodes once the record is complete, so this survives; a parser that decodes line by line raises UnicodeDecodeError on the file.

2. Bare newlines inside values. 49 lines continue a value with a raw newline instead of a CONT record, so they arrive with no level number. repair_bare_newlines re-tags each as the CONT it was meant to be, preserving the line break. This pre-pass runs only after a first parse attempt has failed, so well-formed files are never rewritten.

Deliberate differences from the .ftb path

  • Date qualifiers are normalised. ged4py renders BEF 1856 as BEFORE 1856 and ABT 1762 as ABOUT 1762, where FTB stores the short form. 182 of 4319 dates in kafkova.ged; the rest are byte-identical. year_from/year_to are unaffected.

  • Open-ended bounds match FTB exactly. For AFT/FROM dates the importer writes FTB's own 99999999 "no upper bound", and -99999999 below a BEF date, so year_to and year_from read the same from either source.

  • language becomes a no-op. GEDCOM has one text language, taken from HEAD.LANG. Languages FTB has no number for are assigned one at or above 200 and registered with their real name and ISO code, so a Polish file is not labelled English.

  • tree_name comes from the filename. A GEDCOM's HEAD.FILE is an export description (Exported by MyHeritage.com from … on Sun, 02 Aug 2026), not a name; it is kept as the ExportDescription parameter instead.

  • Nothing is soft-deleted. delete_flag is always 0, since a GEDCOM export contains only live records.

Development

.venv/bin/python -m pytest -q      # 214 tests
.venv/bin/ruff check .
.venv/bin/ruff format --check .

Test data

Tests run against static fixtures in tests/data, not against any real tree:

File

Purpose

ftb_schema.sql

The CREATE TABLE statements of a real FTB file, extracted verbatim

sample.ftb

A 15-person tree built on that schema

sample.ged

The same family as GEDCOM

Both are generated by python -m tests.make_fixtures and checked in, so the suite never depends on the generator having run. Regeneration is byte-for-byte reproducible.

The fixture is small but deliberately awkward: protobuf date and RESI header columns holding bytes copied from genuine rows, a soft-deleted person, connection and fact, an is_alive value that is not in the documented set, text present in one language and missing in the other, every child role and family status, doubly-escaped HTML in notes, a citation whose "page" is really a URL, and a place whose name is blank in the preferred language. That is what lets counts be asserted exactly.

kafkova.ftb and kafkova.ged in the repository root are live working files — the tree gains people whenever the author records one, so no test may assert a count, name or date against them. tests/test_live_files.py uses them when present, for invariants only: that the file opens, that no text decoded to a replacement character, that the relationship graph is reciprocal, that notes and media come back clean, and that every tool payload is JSON-serialisable. It skips when the files are absent.

Those two files are gitignored: they are personal data, not test input. Clone the repository and the suite runs on the fixtures alone.

Continuous integration

ci.yml runs ruff and the suite on Python 3.11 through 3.14, installing from uv.lock with --frozen so CI cannot silently resolve something the lockfile does not pin. It also regenerates the fixtures and fails if a byte changed, which catches the generator drifting away from the files the tests read.

Dependabot proposes weekly updates for the uv and github-actions ecosystems. Patch and minor bumps are grouped into one pull request and merge themselves once CI is green; major bumps arrive individually and wait for review, because a passing suite only evidences the behaviour the tests already cover.

Licence

Apache License 2.0. The reverse-engineering notes below describe MyHeritage's file format; they are not affiliated with or endorsed by MyHeritage.


Format notes

Everything below was derived by inspecting a real file (kafkova.ftb, FTB 8.0.0.8640, db_version 1.7, GEDCOM 5.5.1 dialect FTBDB, UTF-8). Values are marked UNVERIFIED where they did not occur in that file.

The row counts quoted below are the evidence each mapping was established from, measured against that tree as it stood at the time. kafkova.ftb is a live file and has grown since, so treat the counts as provenance for a conclusion, not as current statistics.

Table layout

FTB uses a consistent three-layer pattern:

  • *_main_data — identity, flags, dates, foreign keys

  • *_lang_data — all human-readable text, keyed by data_language

  • *_connection — many-to-many joins

Soft deletion is pervasive: nearly every table has delete_flag, and rows with delete_flag = 1 must be filtered out. Soft-deleted family connections would otherwise appear as phantom relatives.

It is also two levels deep, which is easy to miss. FTB deletes a person by flagging their individual_main_data row and leaves the facts, citations and media hanging off them with delete_flag = 0 of their own. Filtering only the child table therefore still counts data belonging to someone who is no longer in the tree — and if a numerator does that while its denominator does not, the result can exceed 100% of the tree. Any aggregate over facts has to join back to the owner and check its flag too.

Languages

project_parameters.project_languages is a protobuf blob; 0A 02 00 14 decodes to field 1 = bytes [0, 20], the list of language codes in use.

Code

Language

0

English

20

Czech

Text rows exist per language and are frequently missing for one of them, so any read needs a fallback chain.

Polymorphic references

Notes, citations and media attach to entities through token_on_item(entity_id, item_type) rather than direct foreign keys.

item_type

Entity

Evidence in kafkova.ftb

1

Individual

1016 rows; entity_id equals individual_id

2

Family

2 rows

3

Individual fact

30 rows

4

Family fact

3 rows

family_individual_connection.individual_role_type

Verified against gender: every role 2 is male, every role 3 is female.

Value

Meaning

Count

2

husband

540

3

wife

532

5

natural child

1354

6

foster child

1

7

adopted child

1

child_order_in_family holds the author's chosen ordering; -1 means unordered.

family_main_data.status

Verified against co-occurring facts — every status 3 family carries a MARR fact, every status 5 a DIV fact, every status 8 a Death of Spouse event.

Value

Meaning

Count

0

unspecified

95

1

engaged (UNVERIFIED)

2

separated (UNVERIFIED)

3

married

438

5

divorced

6

8

widowed

5

9

life partners

3

individual_main_data.is_alive

Despite the name, this is not a boolean.

Value

Meaning

Count

2

deceased

1655

3

living

209

Facts

individual_fact_main_data.token holds a GEDCOM tag; custom events use EVEN with the real name in fact_type.

Observed: BIRT 1724, DEAT 1658, BURI 299, OCCU 225, CENS 209, RESI 111, CHR 37, BAPM 22, EDUC 22, RELI 18, IMMI 9, NATI 4, PROP 3, DSCR 1, plus custom EVEN subtypes (Settlement, AKA, Hobbies, MYHERITAGE:REL_PARTNERS, …). Family facts: MARR 391, DIV 6.

Three traps

1. date is protobuf, not text — despite the schema comment claiming free text like "22 NOV 1963". The real layout:

0A 0B "19 MAR 1791"          field 1, length 11 — display string
22 2D                        field 4, length 45 — nested date message
   08 01                       modifier (1 = exact, 5 = BETWEEN)
   20 13                       day   = 0x13 = 19
   28 03                       month = 3
   30 FF 0D                    year  = 0x7F | 0x0D<<7 = 1791
   58 E4 0F                    end year for ranges (999999 = none)

The parsed integer columns sorted_date, lower_bound_search_date and upper_bound_search_date (all YYYYMMDD) carry the same information in a far more usable form, so this server reads only field 1 for display and takes structured values from those columns.

Those columns have three distinct ways of saying "there is no date here", and reading any of them as a date invents one:

Value

Meaning

Written for

999999999

unknown or absent

a fact with no date at all

99999999

no upper bound

AFT and FROM dates

-99999999

no lower bound

BEF and TO dates

Each exceeds the magnitude of any real YYYYMMDD — the largest observed is 20250127, and nothing falls between that and 99999999 — so one test on the absolute value recognises all three. Taking 99999999 at face value yields the year 9999, which is larger than every real date and therefore wins any MAX(): it reported AFT 1904 as having an upper bound of 9999, and put 9999 as a tree's latest event year.

2. individual_fact_lang_data.header is protobuf for RESI facts only — 110 of 606 non-empty headers in the sample. Field 1 is address line 1, field 2 the full address. Every other fact type stores plain text in the same column, so the fact's token decides how to read it.

3. Note and citation text is HTML, sometimes escaped twice — notes contain <p> markup and named entities (&scaron;, &iacute;), and some citation descriptions arrive doubly escaped as &amp;lt;br&amp;gt;, needing two unescape passes before the line break appears.

Empty tables

Empty in kafkova.ftb and ignored by this server: album_main_data, album_lang_data, media_item_to_album_connection, repository_main_data, repository_lang_data, task_main_data, task_lang_data, task_to_individual_connection, individual_family_connection_order, intermediate_state, intermediate_state_ids.

intermediate_state* hold uncommitted editor state and are not genealogical data.

A
license - permissive license
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • MCP server for Open Archives: Dutch genealogical records and historical page transcriptions.

  • Bible corpus MCP server: scripture, Greek/Hebrew interlinear data, cross-refs, semantic search.

  • Read-only MCP server for ClassQuill, a tutoring-business-management platform.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mouchar/ftb-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server