open-hydra-mcp
An MCP server that lets an AI assistant add facts it finds in raw text to a hydra archive: an article, a court filing, a press release, notes from an interview. The assistant reads the text and proposes FollowTheMoney records. The server tells it what the archive accepts, checks its proposal, and only then writes it, with the text stored as the source every entity cites.
It doesn't guess. Anything uncertain is left for a person: a name that might be someone already in the archive becomes a merge question in hydra-ctl review, a link to someone the archive doesn't know stays a name, and a role the hierarchy doesn't rank is reported, not rewritten.
What an ingestion writes
Each call to save adds one new collection and nothing else:
collections/<collection>.json the records
.hydra/sources/entries/<collection>.json the raw text, as one Document (an Article when it has a URL)
.hydra/sources/files/<collection>/<file> the original file, when the source is one
Every entity (person, organization, company…) cites that document in sources, so hydra-ctl claims and hydra-ctl sources trace each value back to the text. Relationships have no sources field in FtM: they trace back through their collection, which holds that one source only.
The archive is rebuilt after writing, and the result reports the new review questions. A batch with errors is never written, an existing collection is never overwritten unless asked, and a build that fails puts the files back as they were. To undo a save, delete the files listed in its undo result, then run hydra-ctl build.
Tools
| Tool | What it does |
|---|---|
labelling_guidelines |
How to write records so hydra places them correctly, with this archive's relationship fields and position ladder |
read_file |
A file's text exactly as it will be stored as evidence, in parts; says whether the archive already holds it |
describe_schema |
With no name, the entity types and relationships; with one, every property it accepts, its type, which are required, and what a link takes |
find_entities |
Existing entities closest to a name, so new records link instead of duplicating |
check_records |
Dry run of save: errors, warnings, how links and roles resolve, likely duplicates. Writes nothing |
save |
Checks, writes the batch and its evidence, rebuilds, and reports what changed |
get_records |
Reads a saved collection back with each property's type, what every link resolved to after the build, and the checks repeated on what was written |
link_image |
Previews, then on confirm adds, a hints.yaml rule giving matching entities a portrait, an icon (thumb) or a logo from a local file |
create_archive |
Previews, then on confirm creates, a new empty archive in a new or empty folder (as hydra-ctl init) |
The intended order: guidelines, read_file, describe_schema and find_entities while drafting, check_records, save, then get_records to verify.
Labelling guidelines
The server doesn't try to catch every badly structured record after the fact. It tells the assistant how to write good ones in the first place, from the rules in the hierarchy guide and hydra's data model. labelling_guidelines returns them, and the server's instructions tell the assistant to read them before proposing a record:
- only what the text states: no facts from memory, no inferred relationships
- one record per entity, its fullest name as written; other full names in
alias, surnames and nicknames inweakAlias; two records when unsure, since review merges them - each end of a relationship in the right field and of the right kind, never pointing at itself
- roles in the text's own words, left out when the text gives none, never changed to fit the hierarchy
- Family's word describes the relative from the person's side ("father" puts the relative above)
- an organization inside another takes a role like "affiliate", "unit" or "branch", never a person's rank; a subsidiary is an Ownership
- dates only as given, the start never after the end
- a public office is a Position plus an Occupancy: the holder sits under the office's organization, titled by the office; events link their participants through
involved; addresses stay inaddressunless shared - images, the hierarchy and review decisions left to the archive's owner
The relationship table (which field is which end, what kind of entity each takes, which end hydra draws above) and the position ladder are read from the archive itself: its schema, direction.yaml and hierarchy. With a custom positions.yaml the guidelines show its positions; without one, hydra's defaults.
Records
Records use hydra's FtM dialect: links are written as the linked entity's name, not an id.
[
{"schema": "Person", "properties": {"name": ["Rob Kerr"], "weakAlias": ["Kerr"]}},
{"schema": "PublicBody", "properties": {"name": ["Port Authority"]}},
{"schema": "Employment", "properties": {"employee": ["Rob Kerr"], "employer": ["Port Authority"],
"role": ["deputy"], "startDate": ["2019"]}}
]
id is optional; one is made from the schema and name (person-rob-kerr). Values can be a string or a list. Dates are YYYY, YYYY-MM or YYYY-MM-DD.
check_records blocks on: an unknown or abstract schema, a property the schema doesn't have, a malformed date, a relationship missing one of its ends, an entity without a name, an image field (images are linked in hints.yaml, never in records), and two records sharing an id.
With or without a custom hierarchy
Hydra ranks people inside an organization by the role on their relationship (or their own position), matched against the titles of the positions in effect. labelling_guidelines reports which ladder is in effect:
- defaults only: hydra's four positions (Leader, Second-in-command, Member, Associate). Only leadership roles rank: "deputy" ranks as Second-in-command, "administrator" ranks no one.
- custom and defaults: the archive's
positions.yamladds its own positions (a corporate chain, an agency's ranks) to the defaults. - custom only: the defaults are switched off and
positions.yamlis the whole ladder.
In every mode the assistant keeps each role in the source's own words. check_records and save report the position each role ranks as, and for a role nothing takes, the title to add to positions.yaml if the archive's owner wants it ranked. The data stays faithful to the text, and the hierarchy catches up. See the hierarchy guide.
Changes the owner approves
Images aren't facts from a text, so the assistant never puts them in records. link_image adds a rule to .hydra/rules/hints.yaml that gives matching entities a portrait, an icon (thumb) or a logo. It works in two calls: without confirm it only previews the rule, and the assistant calls it with confirm=true only after you approve. The match is schema, position, titled and/or name, all of which must hold. Local files only, never a URL; the file is copied into .hydra/resources/, a rule that hydra-ctl check flags is not kept, and the archive is rebuilt to say how many entities it reached.
The position ladder is not changed through this server. Roles are kept in the source's words, and save reports the ones that rank no one; the owner edits .hydra/rules/positions.yaml (see the hierarchy guide).
Install
hydra-mcp needs open-hydra (the open-hydra package, which gives hydra-ctl), and Python 3.11 or later. Neither is on PyPI yet, so install both from GitHub:
uv tool install git+https://github.com/meridiangray/open-hydra-mcp --with git+https://github.com/meridiangray/open-hydra
Or with pip, into any environment:
pip install git+https://github.com/meridiangray/open-hydra git+https://github.com/meridiangray/open-hydra-mcp
To update, run the same command again with uv tool install --reinstall or pip install --force-reinstall, and restart your client.
Add it to your client
Every MCP client needs the command hydra-mcp and, optionally, HYDRA_ARCHIVE set to the archive's folder.
Claude Code:
claude mcp add hydra --env HYDRA_ARCHIVE=/path/to/case -- hydra-mcp
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"hydra": {
"command": "hydra-mcp",
"env": {"HYDRA_ARCHIVE": "/path/to/case"}
}
}
}
If the client can't find the command, give its full path (which hydra-mcp or where hydra-mcp prints it).
Windows: give the full path and double every backslash:
"command": "C:\\Users\\you\\.local\\bin\\hydra-mcp.exe",
"env": {"HYDRA_ARCHIVE": "C:\\cases\\case"}
Installed in WSL, client on Windows:
"command": "wsl",
"args": ["-d", "Debian", "--", "bash", "-lc", "HYDRA_ARCHIVE=/mnt/c/cases/case hydra-mcp"]
Running it
The archive is each tool's archive argument, else HYDRA_ARCHIVE, else an archive activated (hydra-ctl activate) in the folder the server starts in or above it, else that folder if it is an archive.
With no archive
If none of those gives an archive, nothing is written anywhere:
- Tools that need an archive say none is selected, and tell the assistant to ask you for one or offer
create_archive. labelling_guidelines,describe_schemaandcheck_recordsstill work, on hydra's defaults, and say so. The assistant can read the text and check its records before an archive exists.find_entitiesfinds nothing, since there is nothing to find.saveandget_recordsrefuse with the same advice.create_archivetakes the full path of a new or empty folder you choose, a name (default: the folder's) and an id prefix (default: made from the name,nonefor one per collection). It previews first and creates the archive only withconfirm=true. The assistant then passes that folder asarchive. To make it the default, setHYDRA_ARCHIVEin the server's configuration.
An archive that is named but isn't one (a wrong path) is an error, never a quiet fallback to the defaults.
Tests
git clone https://github.com/meridiangray/open-hydra
git clone https://github.com/meridiangray/open-hydra-mcp
cd open-hydra-mcp
python3 -m venv .venv && .venv/bin/pip install pytest -e ../open-hydra -e .
.venv/bin/python -m pytest -q
License
Copyright (c) 2026 Meridian Gray. hydra-mcp is free software under the GNU Affero General Public License, version 3 only, as is open-hydra. If you run a modified version as a network service, the AGPL requires you to offer its users the source of your changes.
Contributing
See CONTRIBUTING.md. Before a pull request can merge, you agree once to the Contributor Agreement.