The manual · written for an AI

The tool surface

This page is the technical manual, and it is meant to be read by a machine: every tool lexifye exposes over MCP, with its access level and its complete input schema, generated from the live registry — so what is published here is exactly what is served. An agent can evaluate the whole contract here before anybody asks for a credential.

If you are a person deciding whether Lexifye is for you, you want the short version instead. Nothing on this page is needed to use the web editor.

80 tools · endpoint https://lexifye.publifye.pro/mcp (JSON-RPC 2.0) · connect it to your AI · discovery at /.well-known/oauth-protected-resource

Levels

read buys the tool surface, never other people's dictionaries — a dictionary is reachable only through membership of it, on every plane. write is content editing: the owner, guest editors, and members of an attached group. admin is owner-only per dictionary, or operator-only for the admin_* tools, which are listed for everyone and callable by nobody without an administrative key.

Dictionaries (14)

Create a dictionary and read it back whole. dict_get supports a conditional fetch (if_version) so a consumer polls cheaply.

dict_create

write

Create a new dictionary. Author is stamped from the caller's pubcontacts Contact. author_bio is an optional opt-in paragraph (<=500 chars) printed under the title-page date; default empty. language is an optional BCP-47 tag (e.g. 'grc', 'he', 'nb') for the dictionary's primary language. --- WHO CAN REACH IT --- There is no visibility setting and no public dictionary. Every dictionary is readable AND editable by exactly one set: you, the guest editors you add, and members of a group you attach. Its artifact URLs (.json/.html/.epub/.tex/.pdf) require that same membership. Authorised Publifye services may READ it service-to-service (lexifye is the ecosystem's system of record); they can never write or administer it. --- OWNER (owner_id: SERVICE/ADMIN ONLY) --- Normally omit it: the owner is stamped from the authenticated caller, and a user creating a dictionary owns it. A TRUSTED SERVICE (a registered service key with no idu_ of its own — junifye's bridge is exactly this) has no identity to own a dictionary with, so it passes owner_id = the 'idu…' of the signed-in author it is creating on behalf of. Same trust boundary as dict_list_for_user, and the plan cap is charged to that owner, not the service. A regular user key passing owner_id is refused: you may only create dictionaries for yourself.

input schema
{
  "properties": {
    "author_bio": {
      "maxLength": 500,
      "type": "string"
    },
    "language": {
      "maxLength": 35,
      "type": "string"
    },
    "owner_id": {
      "description": "SERVICE/ADMIN ONLY: the pubhub 'idu…' id to own the new dict (on-behalf create). Omit as a user — the owner is your own identity.",
      "type": "string"
    },
    "title": {
      "type": "string"
    }
  },
  "required": [
    "title"
  ],
  "type": "object"
}

dict_delete

admin

Delete a dict. Requires admin (owner). --- SOFT BY DEFAULT --- The dict moves to the trash: it leaves every listing but keeps every entry, definition, version history and private note, and dict_restore brings it back whole for 30 days. List what is in there with dict_list(include_deleted=true) — you do not need to have kept the id. The plan slot is freed immediately. --- purge=true --- IRREVERSIBLE. Hard-purges the dict, every entry and definition, all indexes, the collab rosters, the audit stream, the rendered artifacts and the <uuid>.json export, right now. Use it only when the content genuinely must not remain — never as a tidier version of the default. It also records a durable PURGE TOMBSTONE, so the dictionary does not come back from the disaster-recovery mirror the way it used to. The mirror is add-only, so the stored copy is SUPPRESSED rather than deleted; ask an operator for admin_purge_tombstone(action:"redact") if the content must actually leave it. The purge is refused outright if the tombstone cannot be recorded — nothing is destroyed in that case.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict.",
      "type": "string"
    },
    "id": {
      "description": "DEPRECATED ALIAS for dict_id. Still accepted; pass dict_id instead.",
      "type": "string"
    },
    "purge": {
      "default": false,
      "description": "IRREVERSIBLE hard delete. Omit for the recoverable default.",
      "type": "boolean"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

dict_enrich

write

Run Darash Strong's enrichment on a dict NOW: scan its definitions for referenced original-language words (Strong's codes in `strong` spans, and hebrew/greek spans carrying a code), pull their lexicon data from darash, and add them as real auto entries (native lemma + SBL transliteration + a composed definition). Manually-authored entries always win a term collision; auto entries whose reference has vanished are reconciled away. depth (optional, 0..4) overrides the dict's strongs_depth for THIS run only: 0 OFF · 1 referenced words only · 2 referenced + one derivation/related hop · 3-4 deeper. depth=0 really is off — nothing is scanned, created or reconciled, and the report says so in `note`; it is NOT the same as omitting depth, which uses the dict's own strongs_depth. A one-shot dict_enrich may go deeper (up to 4) than the automatic background enrich (which never exceeds 2). Also runs automatically (debounced) after definition edits — call this to force it or to request a deeper pass. A pass only ever removes what it could itself have CREATED. An auto entry made by a deeper pass, or one whose creating depth is unknown, is left alone and counted in `kept_deeper` — so a deliberate depth-4 enrichment survives every later depth-2 automatic pass instead of being destroyed by it. And if any single pass ever decides more than 50 entries went orphaned at once, it purges NONE of them and says so in `purge_refused` + `note`. Returns a report: {dict_id, depth, codes_resolved, created, kept, removed, kept_deeper, purge_refused, skipped_manual, skipped_script_spans, capped} — plus `note` when the run deliberately did nothing (depth 0) or refused a mass purge. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "depth": {
      "maximum": 4,
      "minimum": 0,
      "type": "integer"
    },
    "dict_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

dict_freeze

admin

ADMIN. Apply a CANONICAL LOCK to a dictionary — the recoverable take-down. Use this FIRST when responding to an abuse report: it stops the dictionary being changed while you investigate, and destroys nothing. WHILE FROZEN, NOBODY may write to it — not a guest editor, not a group member, and NOT ITS OWNER. Content edits, guest and group management, transfer, delete AND restore are all refused, with a distinct 'frozen' reason so an author is not sent hunting for a permissions problem that does not exist. READING IS UNAFFECTED. dict_get, entry_list and every artifact route keep working for the people already entitled to them. A freeze takes a dictionary out of PLAY, it does not hide it — hiding it is what dict_delete does, and destroying it is admin_dict_delete. Locking the OWNER out of delete/transfer as well is a deliberate divergence from junifye's book_freeze (which locks writes only): a hold the subject of an investigation can dissolve by deleting the evidence, or by handing the dictionary to a second account, is not a hold. An operator keeps every intervention — admin_dict_transfer, admin_guest_remove, admin_dict_delete and dict_unfreeze all still work on a frozen dictionary. Reversible with dict_unfreeze, which puts everything back exactly as it was. Idempotent: re-freezing with the same reason writes no second audit row.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict to freeze (NOT the uuid). Find one with admin_dict_list.",
      "type": "string"
    },
    "reason": {
      "description": "Why the hold exists — a ticket reference and one line (max 200 chars). Strongly recommended: it is shown to the owner in the refusal they will receive, and an unexplained lock generates a support ticket of its own.",
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

dict_get

read

Fetch a dict by id with a PAGE of its entries and their definitions inlined. Each definition includes sha256 — quote it on subsequent definition_update/delete calls. --- PAGINATION (entries) --- limit (default 50, max 200) + offset walk the entries in display order. The reply always carries entry_total, limit, offset and has_more, so a truncated read is never silent: has_more=true means there ARE more entries and you have not seen them. This tool used to inline EVERY entry, which made it unusable on a large dictionary — a 14,153-entry dict failed outright while dict_list and entry_list stayed fast. For metadata only, pass limit=0. For headwords without definition bodies, entry_list is cheaper; to find an entry without knowing its headword, use entry_search. --- WHO MAY READ --- Its owner, its guest editors, and members of any group attached to it — nobody else. There is no public dictionary and no anonymous read. Knowing the id is NOT access; a caller outside that set gets 'forbidden' whether or not the dict exists. Same rule for entry_list, entry_get, definition_get_source, audit_list and every artifact URL. --- CONDITIONAL FETCH (if_version) --- Pass if_version = the `version` you last received and, when the dict has NOT changed since, the reply is the compact {id, version, not_modified:true} instead of the whole tree — the 304 of this API. version bumps on EVERY mutation that changes rendered output (entry add/rename/move/reorder/delete, definition add/update/move/delete/revert, and title/language/author_bio edits), so an unchanged version means unchanged content. Private notes deliberately do NOT bump it — they never render. Omit if_version (or pass a stale one) and the response shape is exactly as before. The not-modified reply carries no artifact URLs, so omit if_version when you need those.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict.",
      "type": "string"
    },
    "id": {
      "description": "DEPRECATED ALIAS for dict_id. Still accepted; pass dict_id instead.",
      "type": "string"
    },
    "if_version": {
      "description": "Conditional fetch: the version you already hold. Equal to the dict's current version =\u003e {id, version, not_modified:true} instead of the payload.",
      "minimum": 1,
      "type": "integer"
    },
    "limit": {
      "default": 50,
      "description": "How many entries to inline, in display order. 0 = metadata only. Check has_more.",
      "maximum": 200,
      "minimum": 0,
      "type": "integer"
    },
    "offset": {
      "default": 0,
      "description": "Entry offset in display order.",
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

dict_group_add

write

Attach a GROUP to a dict so every member of the group becomes a content editor of it (resolved live — membership changes take effect instantly). Dict owner only, and you must BELONG to the group you are attaching. Idempotent. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id from dict_create.id (NOT the uuid).",
      "type": "string"
    },
    "group_id": {
      "description": "The 'grp...' id of a group you belong to (group_list).",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "group_id"
  ],
  "type": "object"
}

dict_group_remove

write

Detach a GROUP from a dict — its members lose the group-derived edit access to that dict (any who are ALSO individual guests, or members of another attached group, keep access via that path). Dict owner only. Idempotent. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id from dict_create.id (NOT the uuid).",
      "type": "string"
    },
    "group_id": {
      "description": "The 'grp...' id to detach (guest_list shows attached groups).",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "group_id"
  ],
  "type": "object"
}

dict_list

read

List the dictionaries you OWN, newest first. There is no public catalogue: filter=public no longer exists, and filter=all is accepted only as a synonym for 'own'. Dictionaries shared WITH you (via a group or a guest grant) are not in this list — the dashboard and the group/guest tools surface those. Paginates via limit (default 50, max 200) + offset. Response shape: {items, total, limit, offset, has_more}. --- TRASH (include_deleted) --- Pass include_deleted=true to ALSO get your soft-deleted dictionaries in an added `deleted` array — id, title, entry/definition counts, deleted_at and days_remaining — so a dict_delete you want to undo can be found without having written its idy… id down. items/total/has_more keep meaning exactly what they meant (the live library); the deleted array is unpaginated and always OLDEST FIRST, so the one closest to grace expiry is at the top. Restore with dict_restore. It is EDITOR-level and OWNER-SCOPED: it needs a write-capable credential and only ever lists dictionaries you own. filter does not apply to it.

input schema
{
  "properties": {
    "filter": {
      "default": "own",
      "description": "Accepted for compatibility; both values mean 'the dicts you own'.",
      "enum": [
        "all",
        "own"
      ],
      "type": "string"
    },
    "include_deleted": {
      "default": false,
      "description": "Also return YOUR soft-deleted dicts in a 'deleted' array (the account trash bin). Requires a write-capable credential.",
      "type": "boolean"
    },
    "limit": {
      "default": 50,
      "maximum": 200,
      "minimum": 1,
      "type": "integer"
    },
    "offset": {
      "default": 0,
      "minimum": 0,
      "type": "integer"
    }
  },
  "type": "object"
}

dict_list_for_user

read

SERVICE/ADMIN ONLY. List every dictionary a given user can work with — the union of dicts they OWN, are a GROUP member of, and are a GUEST editor of — deduped and each tagged with its relation ('owner' | 'group' | 'guest'), owner-first then by title. This is the on-behalf listing a trusted service (e.g. junifye authenticating as itself) calls to render a dictionary PICKER for a signed-in author, so a human never pastes a raw idy… id. LEAN projection for a dropdown: {id, uuid, title, language, entry_count, version, relation} — NO entries or definitions (use dict_get for those). A user with no dictionaries returns {dicts:[], count:0} — an empty list is a valid answer, not an error. NOT for a regular user asking about themselves: a user lists their own dicts with dict_list (default filter=own). Response: {user_id, dicts, count}. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "user_id": {
      "description": "The pubhub 'idu…' id of the user to list dictionaries for (the signed-in author). Resolve a name with guest_find_user; never an email.",
      "type": "string"
    }
  },
  "required": [
    "user_id"
  ],
  "type": "object"
}

dict_replace

write

Find-and-replace a LITERAL string across EVERY definition of a dictionary — fix a recurring typo, rename a term, update a date. One call instead of one edit per definition. ALWAYS PREVIEW FIRST: preview=true is a DRY RUN that reports what WOULD change and writes nothing. Then re-run without it. WHOLE WORDS BY DEFAULT: find="Gen" hits "Gen" and "Gen." but NEVER "Genesis" — a rename thinks around words, not blind characters. Set whole_word=false for a raw substring replace. Case-SENSITIVE, literal, NOT a regular expression. SPAN INTERNALS ARE PROTECTED, and this differs from junifye's book_replace on purpose. Only the PROSE a reader sees is rewritten (text, *emph*, **bold**, and a link's visible label). A Strong's code [H2617], the code/transliteration/original word inside [h:…] and [g:…], a [ref:John 3:16] target and a link's URL are STRUCTURE and are left untouched — so replacing "John" does not silently rewrite your Bible references, and replacing "hesed" does not rewrite a transliteration. To change a headword use entry_rename; to change a span's internals use definition_set_source. PER-DEFINITION MANIFEST: units[] reports applied (with count) | would-apply (preview) | skipped-unchanged | rejected-invalid | not-editable | skipped. Each definition is ATOMIC: the result is re-validated first, and one whose result would be malformed (or over the 64 KB definition limit) is reported rejected-invalid and left exactly as it was. UNDOABLE: every changed definition is checksum-gated against concurrent editors and creates a new revertable version — definition_history / definition_revert put any single one back. Soft-deleted entries and definitions are not touched and are not listed; they are frozen by design (see trash_list). The audit stream records ONE dict_replace row for the whole run, not one per definition — a 213-definition edit is one editorial decision, and per-definition detail lives in definition_history. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict.",
      "type": "string"
    },
    "find": {
      "description": "Exact literal text to find (case-sensitive, not a regex).",
      "minLength": 1,
      "type": "string"
    },
    "preview": {
      "default": false,
      "description": "Dry run: report what would change and write NOTHING. Always preview a rename first.",
      "type": "boolean"
    },
    "replace": {
      "description": "Replacement text. May be empty to delete the word.",
      "type": "string"
    },
    "whole_word": {
      "default": true,
      "description": "Match whole words only (DEFAULT true — the safe rename). false = raw substring replace.",
      "type": "boolean"
    }
  },
  "required": [
    "dict_id",
    "find",
    "replace"
  ],
  "type": "object"
}

dict_restore

admin

Restore a soft-deleted dict, with every entry, definition, version history and private note exactly as they were. Works for 30 days after dict_delete. Requires admin (owner), like dict_delete. Find the id with dict_list(include_deleted=true) — the trash bin of your account. The PLAN CAP applies: a restore occupies a concurrent-dict slot exactly as a create does, so restoring while you are already at your limit is refused, naming what to delete or purge first. (It is refused, never partial — the dictionary stays in the trash for the rest of its 30 days.)

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the soft-deleted dict, from dict_list(include_deleted=true).",
      "type": "string"
    },
    "id": {
      "description": "DEPRECATED ALIAS for dict_id. Still accepted; pass dict_id instead.",
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

dict_set_field

write

Atomically set ONE field on a dict by key. There is NO visibility key: dictionaries are never public, so there is nothing to publish. A dictionary is readable and editable by its owner, its guest editors, and members of an attached group — and by nobody else. --- PRESENTATION KEYS (OWNER ONLY) --- How the dictionary TYPESETS, in every format. Owner-only for the same reason `language` is: they decide whether and how the document sets at all, so a guest editor must not be able to break every PDF of a dict they do not own. page_size a4 (screen default) | letter | 6x9 (trade print) font_size body size in points, 8..18. Below 8 a lexicon is unreadable; above 18 is a large-print edition, which is a different book. entry_break continuous (default) | page = a new page per entry. Suits a few long articles; ruinous for thousands of short ones. interlink true|false (on/off accepted). When on, a word used in a definition becomes a link to its own entry in this dictionary — in the reader, the EPUB and the reader PDF; suppressed in the print interior, where a hyperlink is invisible and still costs an annotation. Membership in this dictionary is the whole test: a Strong's code is linked exactly like a headword, and matching uses the same fold as entry_get (unpointed Hebrew finds the pointed lemma, g26/G0026 land on one entry). An entry never links to itself. Absent is OFF: it changes the text a reader sees. interlink_scope all (default) | first. Which occurrences link, once interlink is on. all = EVERY occurrence. first = only the first mention of a given entry within each entry. Absent means all. Measured on a 128-entry themed word-study dictionary, all vs first was 48 links against 43, over the same 25 pages, and moved not one line break. On a GENERAL dictionary it is a different matter: where the headwords are ordinary words (a, the, of, he, one, man, god, word, love, life…), a 50-entry sample simulated 3,328 links under `first` alone — one every ten words in the worst entry — and `all` is higher still. For that kind of content set `first`, or leave interlink off, and render ONE PAGE before rendering the rest. Read them back in dict_get under `layout`. Omitted keys keep the shipped defaults — a dictionary that never sets one renders exactly as it always has. --- AUTHOR-BIO KEY (OWNER ONLY) --- author_bio (string, 0..500 chars). Opt-in paragraph printed in italic below the title-page date. Empty clears it. NEVER auto-populate — only set when the author explicitly asks to. --- LANGUAGE KEY (OWNER ONLY) --- language (BCP-47 tag, e.g. 'grc', 'he', 'nb'; empty clears). The dictionary's primary language, and the switch that decides how the document TYPESETS — an RTL tag flips the direction of every rendered format. A guest editor may change content but not that, so this key is owner-only (a guest gets 'forbidden'), unlike title and strongs_depth. --- TITLE KEY (editor level: owner, guest editors, group members) --- title (1..200 chars). The dictionary's name, on every cover and dashboard row. --- STRONGS-DEPTH KEY (editor level) --- strongs_depth ('0'..'4' as a string). Darash Strong's auto-enrichment depth: 0 off · 1 referenced words only · 2 default (automatic, content-driven) · 3-4 deeper on request. Depth >2 never runs automatically — the background enrich clamps to 2; request a deeper one-shot with dict_enrich. value is always a string. --- OPTIMISTIC LOCK --- For the content fields title, language, and author_bio you MUST pass `expected` = the field's CURRENT value (from dict_get). Field-scoped: an unrelated field change never conflicts. A mismatch is rejected (stale) with the current value so you can refetch and retry. strongs_depth takes no expected.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict.",
      "type": "string"
    },
    "expected": {
      "description": "REQUIRED for title/language/author_bio: the field's current value (optimistic lock).",
      "type": "string"
    },
    "id": {
      "description": "DEPRECATED ALIAS for dict_id. Still accepted; pass dict_id instead.",
      "type": "string"
    },
    "key": {
      "enum": [
        "author_bio",
        "title",
        "language",
        "strongs_depth"
      ],
      "type": "string"
    },
    "value": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "key",
    "value"
  ],
  "type": "object"
}

dict_transfer

write

Hand ownership of YOUR dictionary to another user. Owner only — the mirror of group_transfer, which already lets you hand over a group. THE RECIPIENT MUST ALREADY BE A COLLABORATOR on this dict: an individual guest editor, or a member of a group attached to it. That is the same consent shape group_transfer uses ("the new owner must already be a member") and it is what stops a dictionary being pushed onto somebody who never agreed to hold it. Add them first with guest_add, or attach a group you both belong to with dict_group_add. WHAT MOVES: the dictionary leaves your library and joins theirs, and every owner-only right goes with it — delete/restore, guest and group management, language, author_bio, and this tool. You keep NOTHING unless you are also a guest or in an attached group. WHAT STAYS: guest editors and attached groups carry over, so the work in progress is not interrupted. The recipient's own guest grant becomes redundant and is dropped (an owner is not their own guest). The response reports both counts — the new owner can drop any of it with guest_remove / dict_group_remove. The recipient is NOTIFIED (notice_list). This is not reversible by you afterwards: only the new owner can transfer it back.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of a dict you own (NOT the uuid).",
      "type": "string"
    },
    "new_owner_id": {
      "description": "DEPRECATED ALIAS for to_owner. Still accepted; pass to_owner instead.",
      "type": "string"
    },
    "to_owner": {
      "description": "The recipient's lexifye user id (pubhub 'idu...') from guest_list or guest_find_user. NOT an email. (admin_dict_transfer's to_owner also resolves a username; this owner-facing door takes the id.)",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "to_owner"
  ],
  "type": "object"
}

dict_unfreeze

admin

ADMIN. Lift a dict_freeze. Everything returns exactly as it was — the freeze changed no content, so there is nothing to restore. Idempotent: unfreezing a dictionary that is not frozen is a no-op and says so. ADMIN-ONLY, with no owner-reopenable case. junifye's book_unfreeze is write-level because junifye ALSO auto-locks a book after 30 days of inactivity and an owner must be able to reopen their own soft lock. lexifye has no auto-lock: the only thing that ever freezes a dictionary here is an operator, so the only thing that lifts one is an operator. A write-level unfreeze would hand the subject of an investigation the key to their own hold.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the frozen dict. List open holds with admin_dict_list(frozen:true).",
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

Entries (9)

Lemmas. Term identity keeps Hebrew nikud while lookup folds it, so entry_get returns {count, matches} rather than pretending a lookup is unique.

entry_add

write

Add a term entry to a dict. term must be unique within the dict (case-insensitive). translit is an optional romanization of the term (e.g. 'logos' for λόγος) for glossary rendering. --- SOURCE (one-call authoring) --- source is optional prose: pass it and the entry's FIRST definition is created in the same call, so a term with a gloss costs one call instead of entry_add + definition_add. WHAT GOES IN IT: the definition's text as MARKUP, which is PARSED rather than stored verbatim — blank lines split it into paragraphs, and the full markup grammar is available (*emph*, **bold**, [H1234], [h:…], [g:…], [ref:…], [a:text|url], # headings, - lists). It goes through exactly the same parser as definition_add_source, so anything valid there is valid here; call source_syntax (or house_style) for the grammar. `body` and `content` are accepted aliases for this same field; passing two spellings at once is refused rather than resolved, because two bodies of prose are two documents. Markup errors are reported BEFORE the entry is created, so a rejected source never leaves a bare entry behind. Omit source to create the term alone and add senses later with definition_add / definition_add_source. A structured `content` BLOCK ARRAY is still refused: entry_add takes text, definition_add takes blocks.

input schema
{
  "properties": {
    "body": {
      "description": "Accepted ALIAS for source — the same field, under the name a sibling tool uses for it. Pass source; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "content": {
      "description": "Accepted ALIAS for source — the same field, under the name a sibling tool uses for it. Pass source; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "source": {
      "description": "Optional. The entry's first definition as markup source (blank-line-separated blocks); PARSED, not stored verbatim. See source_syntax.",
      "type": "string"
    },
    "term": {
      "type": "string"
    },
    "translit": {
      "maxLength": 256,
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "term"
  ],
  "type": "object"
}

entry_delete

write

Delete an entry and all of its definitions from a dict. --- SOFT BY DEFAULT --- The entry moves to the dict's trash: it disappears from entry_list, dict_get, the rendered PDF/HTML/EPUB and the JSON export, but nothing is destroyed. Its definitions, their full version history and the private notes anchored to it all wait with it, and entry_restore brings the lot back for 30 days. The TERM is released immediately, so you can re-add it at once. Browse the bin with trash_list. --- purge=true --- IRREVERSIBLE. Destroys the entry, every definition under it and every definition's version history, right now. Use it only when the content genuinely must not remain. Its private notes are then PROMOTED to dict level rather than deleted — the research about a lemma outlives the lemma. Pass delete_notes=true to destroy them too.

input schema
{
  "properties": {
    "delete_notes": {
      "default": false,
      "description": "Only meaningful with purge=true: destroy the entry's private notes instead of promoting them to dict level.",
      "type": "boolean"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "purge": {
      "default": false,
      "description": "IRREVERSIBLE hard delete. Omit for the recoverable default.",
      "type": "boolean"
    }
  },
  "required": [
    "dict_id",
    "entry_id"
  ],
  "type": "object"
}

entry_get

read

Look a term UP in a dict and return the matching entry (or entries) with their definitions inlined — the read you want when you know the word but not its idj_ id. Read-level: any caller with the dict id may look up. --- MATCHING --- Identity first, then a folded fallback, so you find the word however you type it: Strong's ignores case and zero-padding (H0007 finds H7), Greek ignores accents/breathings and final-vs-medial sigma, Latin ignores case. Hebrew nikud folds for LOOKUP but not for identity — חֵסֵד (H2617, the noun) and חָסַד (H2616, the verb) are genuinely different words, so searching the unpointed חסד legitimately returns BOTH. --- RESULT --- Always {dict_id, term, count, matches:[entry…]} with matches ordered by display order (seq). count is normally 1; count>1 means the folded fallback resolved to several distinct lemmas — pick by translit/definition, never assume matches[0]. A term with no match is an ERROR (not_found), like every other by-identifier read on this surface — it never comes back as an empty success you might mistake for an empty entry. This tool needs the WHOLE term. If you only have a fragment, or you are looking for a word used INSIDE a definition, call entry_search instead.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "term": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "term"
  ],
  "type": "object"
}

entry_list

read

List a dict's entries (summary only: id, term, translit, seq — NO definitions; use dict_get for the full tree). Paginated via limit (default 100, max 500) + offset. Response: {items, total, limit, offset, has_more, sort}. Read-level: any caller with the dict id may list. --- SORT --- sort=seq (default) — the authored display order, the one entry_move / entry_reorder maintain. sort=alpha — alphabetical by term. This is the SAME collation the rendered PDF/EPUB/HTML letter index uses, so an alpha page matches what a reader sees; for a multi-script lexicon it clusters by script (Latin, then Greek, then Hebrew, …) and by letter within each. Pagination means the same thing under either sort: total is the whole entry count and has_more reports whether entries remain past this page. --- TRASH --- Soft-deleted entries are NEVER in items. Pass include_deleted=true to get them alongside, in an added `deleted` array (unpaginated, each with deleted_at and days_remaining) — items/total/limit/offset/has_more keep meaning exactly what they meant. trash_list is the fuller view (it covers deleted definitions too). include_deleted requires EDITOR access to the dict even though plain entry_list is read-level: the trash holds content the author has already removed from every reading surface. --- LOOKING FOR SOMETHING SPECIFIC? --- Do not page a whole dictionary to find one word. entry_get resolves an EXACT term; entry_search finds entries by a text FRAGMENT (and, with content=true, by words inside definitions).

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "include_deleted": {
      "default": false,
      "description": "Also return the dict's soft-deleted entries in a 'deleted' array (the trash bin).",
      "type": "boolean"
    },
    "limit": {
      "default": 100,
      "maximum": 500,
      "minimum": 1,
      "type": "integer"
    },
    "offset": {
      "default": 0,
      "minimum": 0,
      "type": "integer"
    },
    "sort": {
      "default": "seq",
      "enum": [
        "seq",
        "alpha"
      ],
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

entry_move

write

Reorder an entry within its dict's display order. to_index is 0-based (0 = first) and clamped to the valid range. Returns {ok, entry_id, to_index} where to_index is where the entry ACTUALLY landed — when the request was out of range the reply also carries requested_index, clamped:true and a note.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "to_index": {
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "to_index"
  ],
  "type": "object"
}

entry_rename

write

Rename an entry's term (and optionally its transliteration). term must stay unique within the dict (case-insensitive) — a collision with a DIFFERENT entry is rejected. Pass translit to also set/clear the romanization (empty string clears it); omit translit to leave it unchanged. Optimistic lock: pass sha256 = the entry's current lock token (the sha256 field on dict_get / entry_list entries, over term+translit); a mismatch is rejected (stale) with the current sha so you refetch and retry.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "sha256": {
      "description": "The entry's current (term+translit) lock token from dict_get/entry_list.",
      "type": "string"
    },
    "term": {
      "type": "string"
    },
    "translit": {
      "maxLength": 256,
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "term",
    "sha256"
  ],
  "type": "object"
}

entry_reorder

write

Set a dict's ENTIRE entry display order in one call — the bulk twin of entry_move (which repositions one entry and costs N calls to reorder N entries). entry_ids is the new order, first to last. It must name EVERY entry of the dict exactly once: an unknown id, a repeated id, or a missing one is rejected and NOTHING is written, so a partial list can never leave the dict in an order you did not ask for. Call entry_list (limit=500, sort=seq) for the current ids, reorder that array, and send it back. Returns {ok, dict_id, count}.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "entry_ids": {
      "description": "Every entry id of the dict exactly once, in the desired display order.",
      "items": {
        "type": "string"
      },
      "type": "array"
    }
  },
  "required": [
    "dict_id",
    "entry_ids"
  ],
  "type": "object"
}

entry_restore

write

Restore a soft-deleted entry to its original position, with every definition, every definition's version history, and the private notes that were anchored to it. Works for 30 days after entry_delete. Call trash_list for what is restorable. --- TERM COLLISIONS --- A restore is NEVER refused. If another entry took the term while this one sat in the trash, the incumbent KEEPS the uniqueness slot and the response names it as term_taken_by (trash_list flags the same thing as term_taken before you restore). The restored entry still comes back complete — in entry_list, dict_get, the export and every rendered artifact — but entry_get for that exact term will resolve to the incumbent, since lookup is identity-first. Resolve it with entry_rename on whichever of the two should change.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id"
  ],
  "type": "object"
}

entry_search

read

Find entries in ONE dictionary by a text fragment — the way to locate an entry when you do NOT know its exact headword (entry_get needs the exact term; this does not). MATCHING: literal substring, CASE- AND DIACRITIC-BLIND, so q="hesed" finds "ḥesed", q="dap" finds "dāp" and q="jorn" finds "Jørn". Not a regular expression — a literal fragment is what you want here and it is what you get. WHAT IS SEARCHED: entry terms and transliterations always (cheap). Pass content=true to ALSO search definition bodies — slower, since it reads every definition of the dictionary. RESULTS ARE METADATA, NOT CONTENT. Each hit is an ADDRESS: {entry_id, term, translit, where, definition_id?, matches}. `where` is "term", "translit" or "definition"; one entry can produce several hits. There are deliberately NO bodies and no snippets — fetch the ones you want with entry_get (by term) or definition_get_source (by definition_id). That keeps a 200-hit search in kilobytes and lets you pay only for what you read. Definition matching walks the WORDS A READER SEES, not the markup: a Strong's code, a [ref:…] target and a link URL are structure and are not searchable this way (so q="ref" does not match every Bible reference). Look a Strong's code up with entry_get — enrichment materialises codes as real entries. Soft-deleted entries and definitions are never returned; the trash is trash_list's job. Paginates via limit (default 20, max 100) + offset, in the dictionary's display order. Response: {hits, total, limit, offset, has_more, scanned, truncated}. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "content": {
      "default": false,
      "description": "Also search definition bodies (slower). Terms and transliterations are always searched.",
      "type": "boolean"
    },
    "dict_id": {
      "description": "The 'idy...' id of the dict to search. Required — there is no cross-dictionary search.",
      "type": "string"
    },
    "limit": {
      "default": 20,
      "maximum": 100,
      "minimum": 1,
      "type": "integer"
    },
    "offset": {
      "default": 0,
      "minimum": 0,
      "type": "integer"
    },
    "q": {
      "description": "Literal text to find. Case- and diacritic-blind; not a regex.",
      "minLength": 1,
      "type": "string"
    },
    "scan_from": {
      "default": 0,
      "description": "Resume the SCAN here. When truncated=true the reply carries next_scan_from; pass it back to continue through the rest of the dictionary.",
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "dict_id",
    "q"
  ],
  "type": "object"
}

Definitions (11)

Senses, as structured blocks and spans. Every mutation is sha256 optimistic-locked; 20 versions are retained for 7 days.

definition_add

write

Add a definition to an entry. content is an array of blocks (paragraph|heading|list) each holding spans (text, emph, bold, hebrew, greek, strong, bref, link). A list block may also carry items — the per-item span groups, so one bullet can hold emphasis, a Strong's code and a Greek word; concatenated they must equal spans, and you omit items when every item is a single span. Prefer definition_add_source, which writes this shape for you from the markup grammar. Returns the created definition including its sha256 token.

input schema
{
  "properties": {
    "content": {
      "items": {
        "properties": {
          "items": {
            "description": "LIST BLOCKS ONLY: the per-item span groups, so one authored item can hold several spans. Concatenated they must equal spans exactly. Omit it when every item is a single span.",
            "items": {
              "items": {
                "properties": {
                  "ref": {
                    "type": "string"
                  },
                  "strong": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  },
                  "translit": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "text",
                      "emph",
                      "bold",
                      "hebrew",
                      "greek",
                      "strong",
                      "bref",
                      "link"
                    ],
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  }
                },
                "required": [
                  "type"
                ],
                "type": "object"
              },
              "type": "array"
            },
            "type": "array"
          },
          "level": {
            "type": "integer"
          },
          "spans": {
            "description": "The block's complete inline run, in reading order.",
            "items": {
              "properties": {
                "ref": {
                  "type": "string"
                },
                "strong": {
                  "type": "string"
                },
                "text": {
                  "type": "string"
                },
                "translit": {
                  "type": "string"
                },
                "type": {
                  "enum": [
                    "text",
                    "emph",
                    "bold",
                    "hebrew",
                    "greek",
                    "strong",
                    "bref",
                    "link"
                  ],
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              },
              "required": [
                "type"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "type": {
            "enum": [
              "paragraph",
              "heading",
              "list"
            ],
            "type": "string"
          }
        },
        "required": [
          "type",
          "spans"
        ],
        "type": "object"
      },
      "type": "array"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "content"
  ],
  "type": "object"
}

definition_add_source

write

Create a definition from markup source in ONE call — the definition_add twin for the source grammar (source_syntax). Parses the markup into content blocks, validates, and appends the definition to the entry. Returns the created definition including its sha256. WHAT GOES IN source: the definition's whole text as MARKUP, which is PARSED into content blocks (blank lines split paragraphs; *emph*, **bold**, [H1234], [h:…], [g:…], [ref:…], [a:text|url], # headings, - lists) rather than stored verbatim — call source_syntax for the grammar. `body` and `content` are accepted aliases for this same field; passing two spellings at once is refused, and structured block arrays go to definition_add, not here. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "body": {
      "description": "Accepted ALIAS for source — the same field, under the name a sibling tool uses for it. Pass source; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "content": {
      "description": "Accepted ALIAS for source — the same field, under the name a sibling tool uses for it. Pass source; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "source": {
      "description": "The definition's full text as markup source; PARSED, not stored verbatim. See source_syntax.",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "source"
  ],
  "type": "object"
}

definition_delete

write

Delete a definition. sha256_of_old MUST match the current stored sha256. --- SOFT BY DEFAULT --- The definition moves to the entry's trash: it leaves the sense order and every rendered artifact, but its content and its WHOLE VERSION HISTORY stay — definition_history and definition_diff go on answering for it, and definition_restore puts it back at its original position for 30 days. (That is the point: before this, history protected you against a bad edit and gave you nothing at all against a delete.) Browse the bin with trash_list. --- purge=true --- IRREVERSIBLE. Destroys the definition and every retained version of it, right now.

input schema
{
  "properties": {
    "def_id": {
      "description": "DEPRECATED ALIAS for definition_id. Still accepted; pass definition_id instead.",
      "type": "string"
    },
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "purge": {
      "default": false,
      "description": "IRREVERSIBLE hard delete, version history included. Omit for the recoverable default.",
      "type": "boolean"
    },
    "sha256": {
      "description": "Accepted alias for sha256_of_old — the same optimistic-lock token, under the name definition_set_source / definition_revert / entry_rename use for it. Errors from this tool name sha256_of_old.",
      "type": "string"
    },
    "sha256_of_old": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id",
    "sha256_of_old"
  ],
  "type": "object"
}

definition_diff

read

Compare two versions of a definition (from/to version numbers via definition_history), returning each version's markup source plus a unified line diff (lines prefixed ' ' unchanged, '-' removed, '+' added). Read-level for a LIVE definition; a soft-deleted one requires editor access, since the diff renders content the author has already deleted. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "from": {
      "minimum": 1,
      "type": "integer"
    },
    "to": {
      "minimum": 1,
      "type": "integer"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id",
    "from",
    "to"
  ],
  "type": "object"
}

definition_get_source

read

Return a definition's content as round-trippable markup source (the grammar source_syntax documents). Pair with definition_set_source for edit-and-push revision. Returns {dict_id, entry_id, definition_id, source, sha256} — quote sha256 on definition_set_source so a concurrent edit is caught. Read-level for a LIVE definition; one whose entry is in the trash requires editor access. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id"
  ],
  "type": "object"
}

definition_history

read

List the retained version history of a definition (the last 20 saves, 7-day window), newest first. Each version carries {n, sha256, author_name, ts} — NO content (call definition_diff to compare, or definition_revert to restore). Read-level for a LIVE definition; a soft-deleted one requires editor access, since its history is content the author has already deleted. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id"
  ],
  "type": "object"
}

definition_move

write

Reorder a definition within its entry's sense order (rendered 1., 2., 3.). to_index is 0-based (0 = first) and clamped. Returns {ok, definition_id, to_index} where to_index is where the definition ACTUALLY landed — when the request was out of range the reply also carries requested_index, clamped:true and a note.

input schema
{
  "properties": {
    "def_id": {
      "description": "DEPRECATED ALIAS for definition_id. Still accepted; pass definition_id instead.",
      "type": "string"
    },
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "to_index": {
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id",
    "to_index"
  ],
  "type": "object"
}

definition_restore

write

Restore a soft-deleted definition to its original position in its entry's sense order, with its version history intact. Works for 30 days after definition_delete. Call trash_list for what is restorable. If the ENTRY is in the trash too, this is refused and entry_restore is the call to make — it brings every definition back with it, and restoring one alone would leave it invisible.

input schema
{
  "properties": {
    "def_id": {
      "description": "DEPRECATED ALIAS for definition_id. Still accepted; pass definition_id instead.",
      "type": "string"
    },
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id"
  ],
  "type": "object"
}

definition_revert

write

Restore a definition to a past version n (from definition_history), recorded as a NEW version — history is never destroyed. Carries the SAME optimistic lock as an edit: sha256 MUST match the definition's CURRENT sha256 (from dict_get). Returns the new current definition. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "n": {
      "minimum": 1,
      "type": "integer"
    },
    "sha256": {
      "description": "The definition's CURRENT sha256 (optimistic lock).",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id",
    "n",
    "sha256"
  ],
  "type": "object"
}

definition_set_source

write

Replace a definition's content by parsing markup source (the grammar source_syntax documents). sha256 MUST match the current stored token (quote the one from definition_get_source / dict_get) — a mismatch means someone else edited it; refetch and retry. Parse errors name the offending block/construct. Returns the updated definition including its new sha256. WHAT GOES IN source: the definition's whole text as MARKUP, which is PARSED into content blocks (blank lines split paragraphs; *emph*, **bold**, [H1234], [h:…], [g:…], [ref:…], [a:text|url], # headings, - lists) rather than stored verbatim — call source_syntax for the grammar. `body` and `content` are accepted aliases for this same field; passing two spellings at once is refused, and structured block arrays go to definition_update, not here. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "body": {
      "description": "Accepted ALIAS for source — the same field, under the name a sibling tool uses for it. Pass source; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "content": {
      "description": "Accepted ALIAS for source — the same field, under the name a sibling tool uses for it. Pass source; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "sha256": {
      "description": "The current sha256 token (optimistic lock). From definition_get_source or dict_get.",
      "type": "string"
    },
    "source": {
      "description": "The definition's full text as markup source; PARSED, not stored verbatim. See source_syntax.",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id",
    "source",
    "sha256"
  ],
  "type": "object"
}

definition_update

write

Replace a definition's content. sha256_of_old MUST match the current stored sha256 — quote the one returned by dict_get. Mismatch is rejected (refetch and retry).

input schema
{
  "properties": {
    "content": {
      "items": {
        "properties": {
          "items": {
            "description": "LIST BLOCKS ONLY: the per-item span groups, so one authored item can hold several spans. Concatenated they must equal spans exactly. Omit it when every item is a single span.",
            "items": {
              "items": {
                "properties": {
                  "ref": {
                    "type": "string"
                  },
                  "strong": {
                    "type": "string"
                  },
                  "text": {
                    "type": "string"
                  },
                  "translit": {
                    "type": "string"
                  },
                  "type": {
                    "enum": [
                      "text",
                      "emph",
                      "bold",
                      "hebrew",
                      "greek",
                      "strong",
                      "bref",
                      "link"
                    ],
                    "type": "string"
                  },
                  "url": {
                    "type": "string"
                  }
                },
                "required": [
                  "type"
                ],
                "type": "object"
              },
              "type": "array"
            },
            "type": "array"
          },
          "level": {
            "type": "integer"
          },
          "spans": {
            "description": "The block's complete inline run, in reading order.",
            "items": {
              "properties": {
                "ref": {
                  "type": "string"
                },
                "strong": {
                  "type": "string"
                },
                "text": {
                  "type": "string"
                },
                "translit": {
                  "type": "string"
                },
                "type": {
                  "enum": [
                    "text",
                    "emph",
                    "bold",
                    "hebrew",
                    "greek",
                    "strong",
                    "bref",
                    "link"
                  ],
                  "type": "string"
                },
                "url": {
                  "type": "string"
                }
              },
              "required": [
                "type"
              ],
              "type": "object"
            },
            "type": "array"
          },
          "type": {
            "enum": [
              "paragraph",
              "heading",
              "list"
            ],
            "type": "string"
          }
        },
        "required": [
          "type",
          "spans"
        ],
        "type": "object"
      },
      "type": "array"
    },
    "def_id": {
      "description": "DEPRECATED ALIAS for definition_id. Still accepted; pass definition_id instead.",
      "type": "string"
    },
    "definition_id": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "entry_id": {
      "type": "string"
    },
    "sha256": {
      "description": "Accepted alias for sha256_of_old — the same optimistic-lock token, under the name definition_set_source / definition_revert / entry_rename use for it. Errors from this tool name sha256_of_old.",
      "type": "string"
    },
    "sha256_of_old": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "entry_id",
    "definition_id",
    "content",
    "sha256_of_old"
  ],
  "type": "object"
}

Markup source (2)

The round-trippable text form of a definition: parse(render(x)) == x. The web editor uses the same parse path as the API.

house_style

read

THE one-stop authoring guide — read this ONCE before writing a dictionary and you have everything: lexifye's conventions (the translit-always rule, Darash Strong's enrichment + strongs_depth/dict_enrich, universal optimistic locking, definition version history, private notes, and the access model — owner ∪ guest editors ∪ group members, with no public dictionary and nothing to make public) AND the complete markup grammar (the same content as source_syntax, appended). No arguments.

input schema
{
  "properties": {},
  "type": "object"
}

source_syntax

read

Return the grammar of the round-trippable definition markup used by definition_get_source (read) and definition_set_source / definition_add_source (write): block prefixes (#/##/### headings, - list items, plain paragraphs) and inline spans (*emph*, **bold**, [H1234] Strong's — case- and zero-padding-insensitive on input, canonical on output, [h:hebrew], [g:greek], [ref:John 3:16]). Call this once before authoring so the source you write validates. No arguments.

input schema
{
  "properties": {},
  "type": "object"
}

Collaboration (18)

Groups with instant revoke by indirection, plus per-dict guest editors. A group member reads and writes every dict attached to the group.

group_accept

write

Accept an invite to a group (you become a member and gain content-edit access to every dict the group is attached to). You must have a pending invite — see group_list.pending_invites. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id of a group you were invited to (group_list.pending_invites).",
      "type": "string"
    },
    "user_id": {
      "description": "ADMIN ONLY: accept/decline on behalf of this invited user (support operation). Omit to act as yourself.",
      "type": "string"
    }
  },
  "required": [
    "group_id"
  ],
  "type": "object"
}

group_admin_set

write

Grant or revoke ADMIN on a group member (admins can invite, rename, and remove non-admin members). Owner only. The target must already be a member. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "admin": {
      "description": "true = grant admin, false = revoke admin.",
      "type": "boolean"
    },
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "user_id": {
      "description": "The member's lexifye user id (from group_get.members).",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "user_id",
    "admin"
  ],
  "type": "object"
}

group_autoshare_set

write

Toggle YOUR autoshare on a group. With autoshare ON, every NEW dict you create is automatically attached to this group (its members can edit it). You must be a member. Pass apply_existing=true (with on=true) to ALSO attach the group to ALL dicts you currently own — the response reports how many were newly attached. Turning autoshare OFF only stops FUTURE auto-attach; it does NOT detach dicts already attached (use dict_group_remove for those). --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "apply_existing": {
      "description": "Only with on=true: also attach the group to every dict you already own now. Default false.",
      "type": "boolean"
    },
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "on": {
      "description": "true = auto-attach this group to your future dicts; false = stop auto-attaching.",
      "type": "boolean"
    },
    "user_id": {
      "description": "ADMIN ONLY: set autoshare on behalf of this member (support operation). Omit to act as yourself.",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "on"
  ],
  "type": "object"
}

group_create

write

Create a GROUP — a named set of lexifye users you can later attach to a dict (dict_group_add) so every member becomes a content editor of that dict at once. You become the group's owner (and first member). Invite others with group_invite; they must group_accept before they are members. Returns {id:'grp...', name}. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "name": {
      "description": "A short human name for the group (required, max 80 chars), e.g. 'Lexicon Team'.",
      "type": "string"
    }
  },
  "required": [
    "name"
  ],
  "type": "object"
}

group_decline

write

Decline an invite to a group (drops the pending invite; you do NOT join). You must have a pending invite — see group_list.pending_invites. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id of a group you were invited to (group_list.pending_invites).",
      "type": "string"
    },
    "user_id": {
      "description": "ADMIN ONLY: accept/decline on behalf of this invited user (support operation). Omit to act as yourself.",
      "type": "string"
    }
  },
  "required": [
    "group_id"
  ],
  "type": "object"
}

group_delete

write

Delete a group. Owner only. This detaches the group from every dict it is attached to (those dicts lose group-editing) and removes it for all members. Irreversible. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    }
  },
  "required": [
    "group_id"
  ],
  "type": "object"
}

group_get

read

Get one group in detail: members (with display names), each member's admin/owner flag, member_count, attached_dicts_count, and YOUR role + autoshare. Owners/admins additionally see pending_invites and the attached_dict_ids. Only members (or someone you've invited) may view a group. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_create.id or group_list.",
      "type": "string"
    }
  },
  "required": [
    "group_id"
  ],
  "type": "object"
}

group_invite

write

Invite a lexifye user to a group. Owner or admin only. The invitee is NOT a member until they group_accept (find their user_id with guest_find_user). Rejects inviting an existing member. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "user_id": {
      "description": "The lexifye user id (pubhub 'idu...') from guest_find_user. NOT an email.",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "user_id"
  ],
  "type": "object"
}

group_invite_cancel

write

Withdraw a pending group invite. Owner or admin only. Idempotent, and HONEST about it: the reply carries cancelled:true only when an invite was actually pending. With nothing pending it returns cancelled:false and the invitee is NOT notified — they never get a 'your invitation was withdrawn' notice for an invitation they never had. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "user_id": {
      "description": "The invited user's lexifye user id (from group_get pending_invites).",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "user_id"
  ],
  "type": "object"
}

group_leave

write

Leave a group you are a member of. Leaving also DETACHES the group from every dict YOU own. The owner cannot leave — transfer the group (group_transfer) or delete it (group_delete) first. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    }
  },
  "required": [
    "group_id"
  ],
  "type": "object"
}

group_list

read

List the GROUPS you belong to — [{id, name, owner, member_count, role, autoshare}] where role is owner/admin/member and autoshare is YOUR auto-attach opt-in — plus pending_invites: groups you have been invited to but not yet accepted (group_accept / group_decline). --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {},
  "type": "object"
}

group_member_remove

write

Remove a member from a group. The OWNER may remove any member (except themselves — use group_transfer or group_delete); an ADMIN may remove only non-admin members. Removing a member also DETACHES the group from every dict that member OWNS (their dicts stop being group-editable); dicts owned by others stay attached. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "user_id": {
      "description": "The member's lexifye user id (from group_get.members).",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "user_id"
  ],
  "type": "object"
}

group_rename

write

Rename a group. Owner or admin only. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "name": {
      "description": "New name (required, max 80 chars).",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "name"
  ],
  "type": "object"
}

group_transfer

write

Hand ownership of a group to another MEMBER. Owner only. The new owner must already be a member; you (the old owner) stay a member and keep admin. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "group_id": {
      "description": "The 'grp...' id from group_list.",
      "type": "string"
    },
    "user_id": {
      "description": "The new owner's lexifye user id — must already be a member (group_get.members).",
      "type": "string"
    }
  },
  "required": [
    "group_id",
    "user_id"
  ],
  "type": "object"
}

guest_add

write

Invite a lexifye user as a GUEST EDITOR of a dict — they get CONTENT-edit access (entries/definitions) and the dict appears on THEIR dashboard; they never get delete/restore, group attach/detach, transfer, language, author_bio, or the ability to manage other guests. Owner-only. Find the user_id with guest_find_user. Idempotent (re-adding is a no-op). --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id from dict_create.id (NOT the uuid).",
      "type": "string"
    },
    "user_id": {
      "description": "The lexifye user id (pubhub 'idu...') from guest_find_user. NOT an email.",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "user_id"
  ],
  "type": "object"
}

guest_find_user

write

Find lexifye users by NAME to invite as guest editors or group members. Returns [{user_id, name, username}] for DISCOVERABLE users whose name/handle contains the query. Use the returned user_id with guest_add / group_invite. Empty/blank query returns nothing. Matching ignores case AND diacritics on both sides, so 'jorn', 'Jørn' and 'JØRN' all find the same person (æ→ae, ø→o, å→a, ß→ss, é→e …). TWO KINDS OF USER ARE NEVER RETURNED: (1) YOURSELF — the caller is always excluded, since you never need to invite yourself to your own dict; (2) anyone who has marked themselves not-findable, who never appears in anyone's search. So an empty result means 'nobody else discoverable matches', not 'no such user'. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "query": {
      "description": "A name (or part of one) to search for among lexifye users.",
      "type": "string"
    }
  },
  "required": [
    "query"
  ],
  "type": "object"
}

guest_list

read

List the collaborators of a dict. Returns guests: the individual GUEST EDITORS [{user_id, name}], AND groups: the GROUPS attached to the dict [{id, name, member_count}] (every member of an attached group is also an editor). Owner-only. The owner is NOT in either list. READ level: seeing who can reach your own dictionary is reading, not writing, so a read-scope credential is enough. The OWNER-ONLY gate is unchanged and is what actually protects the roster — guest_add / guest_remove remain write. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id from dict_create.id (NOT the uuid).",
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

guest_remove

write

Remove a GUEST EDITOR from a dict — revokes their content access and drops the dict from their library. Owner-only. Idempotent. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id from dict_create.id (NOT the uuid).",
      "type": "string"
    },
    "user_id": {
      "description": "The guest's lexifye user id (from guest_list).",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "user_id"
  ],
  "type": "object"
}

Notes & notices (7)

Private per-dict/per-entry authoring notes, editor-visible only and never rendered or exported. Plus the in-app notice inbox.

note_delete

write

Delete a PRIVATE note at (anchor, key). Editor-only. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "anchor": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "key": {
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "anchor"
  ],
  "type": "object"
}

note_get

write

Fetch a PRIVATE note (summary + full body) at (anchor, key). Editor-only. Returns {note, content}. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "anchor": {
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "key": {
      "description": "slot; default 'main'",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "anchor"
  ],
  "type": "object"
}

note_list

write

List PRIVATE note summaries (no bodies), newest first. Omit anchor to list the whole dict; pass a dict/entry id to filter to that anchor. Editor-only. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "anchor": {
      "description": "optional dict/entry id filter",
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "limit": {
      "default": 50,
      "maximum": 200,
      "minimum": 1,
      "type": "integer"
    },
    "offset": {
      "default": 0,
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

note_patch

write

Append text to an existing PRIVATE note's body (a convenience wrapper over note_set append=true). Optionally updates the subject. Editor-only. WHAT GOES IN append: the text to ADD to the existing body, as PLAIN TEXT, stored verbatim. Notes are never parsed as markup and never rendered into any artifact, so [h:H2617:ḥesed:חֶסֶד] stays those literal characters. `content`, `body` and `source` are accepted aliases for this same field; passing two spellings at once is refused. The canonical name stays `append` because it says what the call DOES — note_set(content) replaces, note_patch(append) adds. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "anchor": {
      "type": "string"
    },
    "append": {
      "description": "The text to append to the note's body, as PLAIN TEXT — never parsed as markup, never rendered.",
      "type": "string"
    },
    "body": {
      "description": "Accepted ALIAS for append — the same field, under the name a sibling tool uses for it. Pass append; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "content": {
      "description": "Accepted ALIAS for append — the same field, under the name a sibling tool uses for it. Pass append; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "key": {
      "type": "string"
    },
    "source": {
      "description": "Accepted ALIAS for append — the same field, under the name a sibling tool uses for it. Pass append; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "subject": {
      "maxLength": 80,
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "anchor",
    "append"
  ],
  "type": "object"
}

note_set

write

Set (upsert) a PRIVATE authoring note on a dict (anchor=idy…) or entry (anchor=idj…). Notes are editor-only and NEVER render into any artifact or the public JSON — use them for research, TODOs, provenance. key is an optional slot (default 'main') so one anchor can hold several notes. subject is a short one-line header. append=true concatenates onto the existing body instead of replacing. WHAT GOES IN content: the note's body as PLAIN TEXT, stored and returned verbatim. It is NOT parsed as markup and NOT rendered anywhere — write [h:H2617:ḥesed:חֶסֶד] into a note and those literal characters are what you get back, so keep markup for definition_set_source. `body` and `source` are accepted aliases for this same field; passing two spellings at once is refused. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {
    "anchor": {
      "description": "idy… (dict) or idj… (entry) — the note's owner",
      "type": "string"
    },
    "append": {
      "type": "boolean"
    },
    "body": {
      "description": "Accepted ALIAS for content — the same field, under the name a sibling tool uses for it. Pass content; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "content": {
      "description": "The note's body, stored as PLAIN TEXT — never parsed as markup, never rendered.",
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "key": {
      "description": "slot; default 'main'",
      "type": "string"
    },
    "source": {
      "description": "Accepted ALIAS for content — the same field, under the name a sibling tool uses for it. Pass content; passing two spellings at once is refused rather than resolved.",
      "type": "string"
    },
    "subject": {
      "maxLength": 80,
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "anchor",
    "content"
  ],
  "type": "object"
}

notice_dismiss

write

Clear ALL of YOUR pending notices (after reading them with notice_list). Idempotent — dismissing an empty inbox succeeds. Returns {ok:true}. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {},
  "type": "object"
}

notice_list

read

List YOUR pending notices (newest first) — the one-shot lines left when a group membership change affects you (invited to / removed from / handed / deleted-out-from-under a group). Non-destructive: call notice_dismiss to clear them. Returns {notices:[...], count}. --- IDENTIFIERS --- Dict id = 'idy...', entry id = 'idj...', definition id = 'idf...', group id = 'grp...'. Always pass the type-matching id. A dict also has a uuid (8-4-4-4-12) used ONLY in artifact download URLs (which are NOT public — every one takes the same membership gate), never in MCP calls. A user id is a pubhub 'idu...' value (resolve names with guest_find_user), never an email.

input schema
{
  "properties": {},
  "type": "object"
}

History & audit (2)

Who changed what, when — and what is recoverable. Both are editor-tier, not read-tier, because deleting must not widen who can read.

audit_list

write

The dict's CHANGE HISTORY: who changed what, and when. Newest first, paginated. EDITOR-level (write), exactly like trash_list and for the same reason: this is editorial history, not content — it names the people who edited the dictionary — so it is limited to those who could have made the edits (owner, guest editors, group members). Reading a dictionary and knowing who worked on it are separate questions, and the read tier also carries the service-to-service lane, which exists to pull content and not personnel. A soft-deleted dict's history is unreadable until dict_restore brings it back. This is the companion to trash_list: that one says what is still recoverable, this one says who deleted it and when (and, for a row purged after its grace window, that it was the retention sweep rather than a person). --- EVENT SHAPE --- id — the stream cursor; pass it back as `before` for the next (older) page, or as `after` to poll for what has happened since. ts — unix seconds. op — the mutation (dict_create, entry_add, entry_delete, entry_restore, entry_purge, definition_add/update/move/delete/restore/purge, dict_set_*, dict_delete/restore, dict_import, guest_add/remove, group_add/remove). actor / actor_name — the pubhub user who made the change, resolved to a display name where one is known. Background work names itself: system:sweep (a trash row purged after its 30-day grace), system:enrich (darash auto-entries), system:import (a rebuild from the S3 export). entry_id / def_id / fields — what the op touched; `fields` carries the op-specific payload (term, defs, notes_promoted, …). --- RETENTION --- The stream is capped at the newest 2000 events per dict, so it is an operational log, not an archive; `trimmed:true` means older rows have been dropped. What a dict CONTAINED is durable elsewhere (the <uuid>.json export + its versioned S3 object); how a definition changed is definition_history.

input schema
{
  "properties": {
    "actor": {
      "description": "Optional exact actor filter (an idu... user id, or system:sweep / system:enrich / system:import).",
      "type": "string"
    },
    "after": {
      "description": "An event id; returns events strictly NEWER than it (polling for what has changed since).",
      "type": "string"
    },
    "before": {
      "description": "An event id from a previous page; returns events strictly OLDER than it.",
      "type": "string"
    },
    "dict_id": {
      "type": "string"
    },
    "limit": {
      "default": 50,
      "description": "1-200 events (default 50).",
      "type": "integer"
    },
    "op": {
      "description": "Optional exact op filter, e.g. entry_delete.",
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

trash_list

write

The dict's TRASH BIN: everything soft-deleted and still recoverable, oldest first. EDITOR-level (write): a soft delete keeps the content, so the bin is visible only to someone who could edit the dict (owner, guest editor, group member). Deleting must not widen who can read what was deleted. entries[] — each with deleted_at, days_remaining, how many definitions and private notes come back with it, and term_taken (true when another entry has since claimed the term; the restore still works, see entry_restore). definitions[] — each with deleted_at, days_remaining, retained_versions (definition_history and definition_diff still answer for a deleted definition — that is the point of the soft delete) and entry_deleted (true when its entry is in the trash too, in which case entry_restore is the call to make; it brings the definition back with it). Restore with entry_restore / definition_restore. After grace_days the background sweep purges the row for good — and, for an entry, promotes its private notes up to dict level so the research is not lost with the lemma.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    },
    "limit": {
      "default": 50,
      "description": "Bounds EACH of the two collections (entries, definitions) independently.",
      "maximum": 200,
      "minimum": 1,
      "type": "integer"
    },
    "offset": {
      "default": 0,
      "minimum": 0,
      "type": "integer"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

Administration (13)

Operator-only: the durability plane over the disk exports and the S3 mirror, forced re-render, hard purge, and the term-index reindex.

admin_audit_list

admin

ADMIN. Cross-dict forensics: every dict's audit stream merged into one newest-first view — 'what happened anywhere in the last hour', or 'everything this user touched'. Filter with op and/or actor; omit dict_id (audit_list is the per-dict tool). The scan is bounded (2000 dicts, 200 events per dict per pass) and reports what it did NOT cover: `truncated:true` when the dict scan hit its cap, and `failed[]` naming any dict whose stream could not be READ. A stream that failed to read is never reported as a dict with no history. --- EVENT SHAPE --- id — the stream cursor; pass it back as `before` for the next (older) page, or as `after` to poll for what has happened since. ts — unix seconds. op — the mutation (dict_create, entry_add, entry_delete, entry_restore, entry_purge, definition_add/update/move/delete/restore/purge, dict_set_*, dict_delete/restore, dict_import, guest_add/remove, group_add/remove). actor / actor_name — the pubhub user who made the change, resolved to a display name where one is known. Background work names itself: system:sweep (a trash row purged after its 30-day grace), system:enrich (darash auto-entries), system:import (a rebuild from the S3 export). entry_id / def_id / fields — what the op touched; `fields` carries the op-specific payload (term, defs, notes_promoted, …). --- RETENTION --- The stream is capped at the newest 2000 events per dict, so it is an operational log, not an archive; `trimmed:true` means older rows have been dropped. What a dict CONTAINED is durable elsewhere (the <uuid>.json export + its versioned S3 object); how a definition changed is definition_history.

input schema
{
  "properties": {
    "actor": {
      "description": "Optional exact actor filter (idu... / system:sweep / system:enrich / system:import).",
      "type": "string"
    },
    "after": {
      "description": "An event id; returns events strictly NEWER than it.",
      "type": "string"
    },
    "before": {
      "description": "An event id; returns events strictly OLDER than it.",
      "type": "string"
    },
    "limit": {
      "default": 50,
      "description": "1-200 events (default 50).",
      "type": "integer"
    },
    "op": {
      "description": "Optional exact op filter, e.g. entry_purge.",
      "type": "string"
    }
  },
  "type": "object"
}

admin_dict_delete

admin

ADMIN. HARD-purge a dict: the dict, every entry + definition, all indexes (owner/global ZSETs, terms/entries/defs), the collab rosters (attached-group + guest reverse indexes), the uuid reverse index, the audit stream, the on-disk artifacts (light/dark/print PDFs, HTML, EPUB, TeX, <uuid>.json export) and any now-unreferenced content blobs. Distinct from the owner-only SOFT dict_delete (recoverable within the grace window). NOT UNDOABLE THROUGH ANY ORDINARY PATH, and precise about what that means. The purge first records a durable PURGE TOMBSTONE, then destroys. Startup hydrate, rebuild-on-empty-DB and admin_s3_restore all consult it and refuse the uuid, so the dict does not come back from a disaster recovery — which it previously DID. WHAT IT DOES NOT DO: erase the bytes from object storage. The S3 mirror is add-only by design (that is what makes a corrupted export recoverable), so `<uuid>.json` stays in the bucket, and on a versioned bucket so does every earlier version. With anonymous access gone there is no public copy of anything, so that mirror is the ENTIRE safety net — which is exactly why a purge suppresses its object rather than deleting it. If the content must actually leave the live mirror, follow up with admin_purge_tombstone(action:"redact"); removing the historical versions needs a delete-capable principal and a human. Saying 'irreversible' without that distinction was the overclaim this wording replaces. IT PURGES THROUGH A FREEZE, DELIBERATELY. dict_freeze locks a dictionary against every change by everyone, its owner included — but NOT against this. A legal erasure obligation can land on content that is simultaneously under investigation, and content nobody is able to remove is a worse failure than the one a hold prevents. The combination is never silent: the purge tombstone records frozen_at_purge plus the hold's stated reason, and that marker outlives both Redis and the dict's own audit stream, so 'this was destroyed while it was explicitly held' stays answerable afterwards. The purge ABORTS if the tombstone cannot be recorded — a purge nothing has recorded is a purge the mirror undoes. THE ONE WAY BACK, so this is not a hidden trapdoor: an admin can deliberately reverse the suppression with admin_purge_tombstone(action:"release") and then admin_s3_restore that uuid, which brings the dictionary back whole IF its mirror object is still intact and un-redacted. It takes two explicit admin calls and is never automatic — that is the difference between 'irreversible' and 'nothing can ever undo this'. Returns {ok, dict_id, uuid, artifacts_removed, content_gc}.

input schema
{
  "properties": {
    "dict_id": {
      "type": "string"
    }
  },
  "required": [
    "dict_id"
  ],
  "type": "object"
}

admin_dict_list

admin

ADMIN. List EVERY dictionary across ALL owners — the operator inventory. The ordinary dict_list is OWNER-SCOPED by construction (it can only page your own library), so before this tool an operator could act on a dictionary only if somebody handed them its idy… id, and could not answer 'what exists' at all. That is what made orphan recovery impossible to even begin. Paginated and hard-capped (limit 1..200, default 50), newest-updated first. Never dumps the whole corpus. owner_id restricts to ONE owner's library. It is VALIDATED, not merely filtered on: a wrong id-kind or an unknown user is refused rather than answered with an empty list, because 'that owner has no dictionaries' and 'you passed a dict id by mistake' must not look the same. q filters case-insensitively over title / language / owner_id / uuid / id. It matches METADATA only — there is deliberately no cross-owner content search here. Each row carries owner_id, frozen + lock_reason, entry/definition counts, version and updated_at, so who owns what and what is on hold is visible at a glance. LIVE dictionaries only: a soft-deleted one has left the index this pages, exactly as it has left its owner's library.

input schema
{
  "properties": {
    "frozen": {
      "description": "Optional: true lists ONLY dictionaries currently under an operator freeze (the open-holds view); false lists only unfrozen ones. Omit for both.",
      "type": "boolean"
    },
    "limit": {
      "description": "Rows per page, 1..200. Omitted (or 0, which reads as unset) means 50.",
      "maximum": 200,
      "minimum": 0,
      "type": "integer"
    },
    "offset": {
      "minimum": 0,
      "type": "integer"
    },
    "owner_id": {
      "description": "Optional: restrict to one owner's library. Must be an EXISTING owner's pubhub user id (idu…); a dict id, uuid or unknown user is rejected, never answered with an empty list.",
      "type": "string"
    },
    "q": {
      "description": "Optional case-insensitive substring filter over title/language/owner_id/uuid/id.",
      "type": "string"
    }
  },
  "type": "object"
}

admin_dict_transfer

admin

ADMIN. Reassign a dictionary from its current owner to another user. THE ONE INTERVENTION NO OWNER CAN PERFORM: departed staff, a deleted or merged account, or a dictionary orphaned because its owner is gone. Without it an orphaned dictionary is permanently unreachable — nobody can edit, share or delete it, and the operator plane may not act as its owner. to_owner accepts a pubhub USERNAME or user id (idu…), resolved and validated through pubhub. A recipient that does not resolve is REFUSED — an unresolvable id would move the dictionary to a second orphan. Unlike the owner's dict_transfer this does NOT require the recipient to be an existing collaborator, and does NOT apply their plan cap: an operator rescuing an orphan must not be blocked by the recipient's tier, and the recipient of an account merge is by definition not already a guest of their own future dictionary. Works on a SOFT-DELETED dictionary too — it moves between the two owners' trash bins rather than resurrecting it, so the new owner can dict_restore it themselves. Guests and attached groups carry over (the response reports the counts). The recipient is notified. Attributed to the OPERATOR on the audit stream, never to either owner.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict to transfer (NOT the uuid). Find one with admin_dict_list.",
      "type": "string"
    },
    "to_owner": {
      "description": "Recipient: pubhub username or user id (idu...).",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "to_owner"
  ],
  "type": "object"
}

admin_guest_remove

admin

ADMIN. Revoke a GUEST EDITOR's access to any dictionary, across owners — abuse response and offboarding. The owner-facing guest_remove is owner-only and an admin key does not stand in for the owner, so the only way to do this before was X-User-ID impersonation, which recorded the OWNER as having removed them. This door records the OPERATOR. Revokes the individual guest grant and drops the dictionary from that user's library, exactly as guest_remove does. Idempotent, and HONEST about it: removed:false comes back when the user held no guest grant, so a mistyped id is not reported as a successful revocation. IT DOES NOT REVOKE GROUP-DERIVED ACCESS. If the user also belongs to a group attached to this dictionary they keep read+write through that path, and the reply says so (still_has_group_access). Detaching the group is the dict owner's call (dict_group_remove) — an operator who needs the access gone NOW freezes the dictionary, which locks everyone out at once. Works on a frozen dictionary: an operator's own doors are not closed by an operator's own hold.

input schema
{
  "properties": {
    "dict_id": {
      "description": "The 'idy...' id of the dict (NOT the uuid). Find one with admin_dict_list; see the roster with guest_list.",
      "type": "string"
    },
    "user_id": {
      "description": "The guest's lexifye user id (pubhub 'idu...') from guest_list.",
      "type": "string"
    }
  },
  "required": [
    "dict_id",
    "user_id"
  ],
  "type": "object"
}

admin_purge_tombstone

admin

ADMIN. Manage the PURGE TOMBSTONE of a dict uuid — the durable marker that stops the S3 mirror resurrecting a hard-purged dictionary. The mirror is add-only (the service must not delete from a bucket whose versioning is the corruption-recovery story), so a purge suppresses its object rather than removing it. Requires an explicit action; there is no default. action="suppress" — record a tombstone for a uuid. THE ONLY WAY to retro-suppress a dict purged before this mechanism existed: those objects are still in the mirror and admin_s3_verify lists them under orphan[]. Refused for a uuid that is currently LIVE unless force:true, because suppressing a live dictionary means it silently will not survive a disaster recovery. action="release" — REVOKE a tombstone, because a purge can be a mistake. It restores nothing by itself; it only re-permits admin_s3_restore for that uuid. Explicit, audited, never automatic. action="redact" — overwrite the CURRENT mirror object with a stub so it no longer carries the content. For erasure obligations. Requires an active tombstone. Earlier object versions survive in the bucket and need a delete-capable principal to remove; this makes recovery through the service impossible, so run it only after you are certain the purge was correct. Omit uuid entirely to LIST every tombstone instead of changing one. Returns {action, uuid, tombstone} or {tombstones[]}.

input schema
{
  "properties": {
    "action": {
      "description": "Required with uuid. suppress = mark purged; release = revoke the mark; redact = overwrite the mirror object.",
      "enum": [
        "suppress",
        "release",
        "redact"
      ],
      "type": "string"
    },
    "force": {
      "description": "suppress only: allow tombstoning a uuid whose dict is still LIVE. Almost always wrong.",
      "type": "boolean"
    },
    "reason": {
      "description": "Why. Recorded in the tombstone; strongly recommended, since this object is permanent.",
      "type": "string"
    },
    "uuid": {
      "description": "Dict UUID (8-4-4-4-12 hex). Omit to list all tombstones.",
      "type": "string"
    }
  },
  "type": "object"
}

admin_reindex_terms

admin

ADMIN. Rebuild both per-dict term indexes under the CURRENT key derivation: the IDENTITY index (uniqueness — Hebrew KEEPS its nikud, so חֵסֵד and חָסַד stay two entries; Strong's zero-padding and Greek accents/final-sigma DO fold) and the FOLDED lookup index (unpointed Hebrew finds the whole family). This is the supported way to run the term migration — the startup path is opt-in only. DRY RUN by default; pass apply:true to commit. With dict_id: that dict only; without: every dict, and a successful full pass stamps the schema marker. NEVER touches an entry: on an identity collision the lowest-seq entry wins the slot, the loser stays fully intact and stays findable via the folded index, and both are reported in collisions[]. A dict whose entries cannot be READ is skipped with its indexes untouched and reported in details[].error — it is never treated as empty. Returns {dry_run, version, dicts, entries, changed, collisions, failed, details[]}.

input schema
{
  "properties": {
    "apply": {
      "description": "false (default) = dry run, zero writes. true = commit the rebuilt index.",
      "type": "boolean"
    },
    "dict_id": {
      "description": "Optional. Scope the reindex to one dict (idy...). Omit for every dict.",
      "type": "string"
    }
  },
  "type": "object"
}

admin_rerender

admin

ADMIN. Rebuild rendered artifacts. With dict_id: QUEUE that dict's artifacts (light+dark PDF, HTML reader, EPUB3, print interior) and return immediately with each one's job state plus the URLs they will appear at. Rendering is asynchronous and DEDUPLICATED — at most one render per (dict, variant) is ever in flight, so calling this twice does not start two. Pass wait:true to block until they finish; that is for small dictionaries and scripts only, because a large one takes minutes and will time out your client (14,153 entries did exactly that). Without dict_id: clear EVERY dict's built_version sidecars so all artifacts lazily re-render on next fetch (a cheap fleet-wide cache bust, e.g. after a renderer change). Returns {dict_id, uuid, jobs[], urls} or {cleared}.

input schema
{
  "properties": {
    "dict_id": {
      "description": "Optional. A dict id (idy...) to rebuild. Omit to clear all sidecars for lazy re-render.",
      "type": "string"
    },
    "wait": {
      "description": "false (default) = queue the renders and return at once. true = block until every artifact is built; a large dictionary WILL exceed a typical MCP client timeout.",
      "type": "boolean"
    }
  },
  "type": "object"
}

admin_s3_restore

admin

ADMIN. Restore dicts from the S3 mirror into Redis (and disk). With uuid: restore that one dict. Without: restore EVERY dict currently missing from Redis. A dict already live is skipped — NEVER overwritten. A HARD-PURGED dict is REFUSED and named in purged[]: it is missing from Redis precisely because someone destroyed it, and the mirror keeps the object only because the mirror is add-only. Undo that deliberately with admin_purge_tombstone(action:"release") if the purge was a mistake. A uuid whose liveness cannot be read is reported in failed[] rather than restored over. Returns {restored, skipped_present, purged[], failed[]}.

input schema
{
  "properties": {
    "uuid": {
      "description": "Optional dict UUID (the 8-4-4-4-12 hex from the public download URL). Omit to restore all missing dicts.",
      "type": "string"
    }
  },
  "type": "object"
}

admin_s3_status

admin

ADMIN. Durability dashboard. Returns {enabled, redis_count, disk_count, s3_count, tombstones, disk_not_in_s3[], s3_not_on_disk[]} so an operator can see how Redis, the disk export dir, and the S3 mirror compare and where they drift. `tombstones` is the number of hard-purged uuids the mirror still holds objects for but which no recovery path will ever import — the mirror is add-only, so a purged dict's object stays and is suppressed instead. Read-only.

input schema
{
  "properties": {},
  "type": "object"
}

admin_s3_sync

admin

ADMIN. Add-only two-way reconcile of the local export dir and the durable S3 mirror. Uploads local-only dicts to S3 and downloads S3-only dicts to disk; NEVER deletes from either side. The purge tombstones reconcile on the same terms and in the same pass — a node that pulled the exports without the suppressions would rebuild hard-purged dicts. Returns {enabled, uploaded, downloaded, in_sync, local_total, s3_total, tombstones_uploaded, tombstones_downloaded}. No-op (enabled=false) when S3 or the export dir is not configured.

input schema
{
  "properties": {},
  "type": "object"
}

admin_s3_verify

admin

ADMIN. Integrity-check the S3 mirror: every <uuid>.json must parse as a valid dict export and its version must match the live dict. Read-only. Returns {checked, ok, stale[], corrupt[], orphan[], purged_suppressed[], tombstoned_live[], unknown[], redacted[]}: stale — mirror version older than the live dict corrupt — not valid dict-export JSON orphan — in S3, no live dict, NO purge tombstone. THE REAL ANOMALY: nobody recorded destroying this and nothing explains its absence. Investigate. purged_suppressed — in S3, no live dict, hard-purged on the record. EXPECTED state, not a fault: the mirror is add-only so the object stays, and every restore path refuses it. tombstoned_live — BOTH tombstoned and live. The inverted anomaly: this dict works today and would NOT survive a disaster recovery. Release the tombstone or re-run the purge. unknown — liveness could not be READ. Never counted as an orphan: 'I could not tell' is not 'there is no dict'. redacted — mirror object deliberately overwritten by admin_purge_tombstone(action:"redact").

input schema
{
  "properties": {},
  "type": "object"
}

admin_system_status

admin

ADMIN. The operator rollup, in one call — everything a health investigation previously had to assemble by hand from curl /health, pubdev versions and four separate admin_s3_* calls. Returns: version + build_id + uptime; redis liveness with latency; CORPUS counts (live dictionaries, entries, definitions, and how many are under an operator freeze); DURABILITY (redis vs disk-export vs S3 object counts, drift both ways, purge tombstones and how many are active); RETENTION (the 30-day delete grace, definition history depth, audit stream cap); PLAN caps; and which integrations are configured. Corpus counts walk the dict index and read summary hashes only — no entries or definitions are loaded — so the cost is proportional to the number of DICTIONARIES, not to their size. A section that cannot be READ is reported as an error on that section, never as a zero: 'the S3 mirror holds nothing' and 'I could not reach the S3 mirror' are different answers and an operator must not have to guess which one they got.

input schema
{
  "properties": {},
  "type": "object"
}

Everything else (4)

clear_server_logs

admin

Drop this service's in-memory log history and report how many entries went. Use it to get a clean window before reproducing a bug. DESTRUCTIVE for the in-memory copy only — the file and remote sinks still hold everything alog ever wrote, so nothing is truly lost.

input schema
{
  "type": "object"
}

get_server_logs

admin

Search this service's SERVER logs from the in-memory ring buffer — the fastest way to debug what the service actually did. This reads MEMORY, never disk: the ring holds far more than the rotated files do, so it is the whole history worth querying. All filters are optional and combine (AND): level is a MINIMUM (WARN returns warnings AND errors); event matches the log category exactly (e.g. 'SCHED', 'IMAGEGEN'); message is a substring of the message, or a regular expression when wrapped in /slashes/; q is free text matching EITHER the message or the category, which is the one to reach for first; data filters on structured fields by exact value, e.g. {"username":"jorn"} — reach for it when the value you have is a field, not prose; since/until accept RFC3339 or a relative duration ('15m', '2h'). ORDER is tail -n: you get the NEWEST matching window, listed oldest-first so it reads chronologically. PAGING: the response returns 'oldest' and 'newest'. To step further back in time pass oldest as 'until'; to step forward pass newest as 'since'. Bounds are inclusive, so the boundary entry repeats once. 'entries_matched' is how many matched before the line and byte caps — when it exceeds 'entries_returned', 'truncated' is true and you are seeing a window, not the whole set. Structured values are shortened by default; set full_data=true once a specific entry is worth reading in full. If you need MORE history than the ring holds, raise it with set_log_capacity before reproducing — do not go looking for log files. alog never records API keys, tokens or secrets, so this output is safe to read. This is lexifye-service — useful categories: RENDER (render jobs: 'render job done'/'render job FAILED' with per-variant durations), PDF and PRINT (artifact serving), ADMIN (operator actions), ENRICH (darash Strong's enrichment), PERSIST (the export sweeper + S3 mirror), STORE, REDIS, PUBHUB, LIGHTHOUSE, STARTUP.

input schema
{
  "properties": {
    "data": {
      "description": "Structured-field filter, exact values, ANDed: {\"username\":\"jorn\"}",
      "type": "object"
    },
    "event": {
      "description": "Exact log category/event, e.g. SCHED",
      "type": "string"
    },
    "full_data": {
      "description": "Return structured values untruncated (default false)",
      "type": "boolean"
    },
    "level": {
      "description": "MINIMUM level to include",
      "enum": [
        "ERROR",
        "WARN",
        "INFO",
        "DEBUG"
      ],
      "type": "string"
    },
    "lines": {
      "description": "Max entries to return, newest kept (default 250)",
      "type": "integer"
    },
    "message": {
      "description": "Substring of the MESSAGE, or /regex/ when wrapped in slashes",
      "type": "string"
    },
    "q": {
      "description": "Free text matching the message OR the category — use this when unsure which",
      "type": "string"
    },
    "since": {
      "description": "Only entries after: RFC3339, or a duration like 15m / 2h",
      "type": "string"
    },
    "until": {
      "description": "Only entries before: RFC3339, or a duration like 5m. Pass the previous response's 'oldest' to page further back",
      "type": "string"
    }
  },
  "type": "object"
}

service_status

read

Liveness and build identity for lexifye-service: {service, version, build_id, status, uptime_seconds, go_version, redis:{ok,latency_ms}, integrations:{...}}. READ level, and DELIBERATELY CORPUS-FREE. It reports whether the service is up, what is running, and which integrations are CONFIGURED (darash enrichment, Lighthouse OAuth, pubhub, pubcontacts, the S3 mirror, the export/content dirs, the renderer) — as booleans. No endpoints, no credentials, no dictionary/entry/definition counts, no owner data. For the operator rollup with corpus counts and mirror state, call admin_system_status. The anonymous /health endpoint is a strict subset of this and stays that way on purpose: it carries {service, status, checks} and NO VERSION. Knowing what version is deployed requires a credential.

input schema
{
  "properties": {},
  "type": "object"
}

set_log_capacity

admin

Change how many log entries this service keeps in memory, at runtime and without a redeploy. Growing keeps everything already buffered; shrinking drops the OLDEST entries and reports how many went. Only the in-memory copy is affected — the file and remote sinks are untouched. Raise it before reproducing a noisy bug; lower it to reclaim memory (~500 bytes per entry).

input schema
{
  "properties": {
    "capacity": {
      "description": "New ring size (clamped to 1000–200000)",
      "type": "integer"
    }
  },
  "required": [
    "capacity"
  ],
  "type": "object"
}

Authoring guide

The same bytes the house_style tool returns.

LEXIFYE HOUSE STYLE — how to author a dictionary well.

A dictionary is a Dict → Entry (lemma) → Definition (sense) tree. Each definition
is structured block/span content authored in the markup grammar (below, and via
source_syntax). Keep entries lean: one lemma per Entry, one sense per Definition
(add more Definitions for more senses — they render 1., 2., 3.).

1. TRANSLIT ALWAYS (house rule). Every Hebrew/Greek word in content SHOULD carry
   a transliteration. Prefer the structured span [h:CODE:translit:word]
   (e.g. [h:H2617:chesed:חֶסֶד]); use [h:translit:word] when you have no Strong's
   code; the bare [h:word] is accepted but comes back with a warning. Include the
   Strong's code whenever you know it — it anchors the word to its lexicon entry
   and drives Darash enrichment. Entry-level transliterations (entry_add /
   entry_rename translit) render in italics beside the headword.

2. STRONG'S ENRICHMENT (Darash). A dict's strongs_depth (dict_set_field) controls
   automatic enrichment: 0 off · 1 referenced words only · 2 default (referenced +
   one derivation/related hop) · 3-4 deeper on request. The service scans your
   definitions for Strong's codes and adds the referenced original-language words
   as real entries (native lemma + SBL translit + a composed gloss), reconciled as
   you edit. Run it on demand (possibly deeper, up to 4) with dict_enrich. Manual
   entries always win a term collision with an auto entry.

3. OPTIMISTIC LOCKING. Content edits carry proof of their base so concurrent
   editors never silently clobber each other: definition_update/set_source and
   definition_revert take the definition's sha256; entry_rename takes the entry's
   sha256 (over term+translit); dict_set_field for title/language/author_bio takes
   the field's current value as `expected` (field-scoped). A stale token is
   rejected naming the current value — refetch (dict_get) and retry. Reorder moves
   (entry_move/definition_move) carry no base and are exempt.

4. VERSION HISTORY. Every definition edit is versioned: definition_history lists
   the retained saves (last 20, 7-day window), definition_diff compares two, and
   definition_revert restores an old one (as a new, sha-locked version).

5. PRIVATE NOTES. note_set/get/list/patch/delete attach private authoring notes to
   a dict or an entry. Notes are editor-visible only and NEVER render into any
   artifact or the exported JSON — use them for research, TODOs, and provenance.

6. LINKS. Cite a source with the link span [a:visible text|https://example.com].
   Only http://, https:// and mailto: targets are accepted — javascript:, data:
   and relative paths are rejected at parse time. Always give the link real
   visible words (never a naked URL): the reader of a PDF or a printed book sees
   only that text, and the same span is what a junifye book's back-matter link
   appendix lists when it imports this dictionary as its glossary.

7. FINDING AND FIXING AT SCALE. entry_search locates an entry when you do not
   know its exact headword: case- and diacritic-blind substring matching over
   terms and transliterations (add content=true to search definition bodies).
   It returns ADDRESSES, not content — fetch the hits you want with entry_get or
   definition_get_source. dict_replace fixes a recurring typo or renames a term
   across every definition in one call: literal, whole-word by default, always
   preview=true first, and SPAN INTERNALS ARE PROTECTED — it rewrites prose and a
   link's visible label, never a Strong's code, a [h:…]/[g:…] span's contents, a
   [ref:…] target or a URL. Use entry_rename for a headword.

8. ACCESS. There is NO publishing step and no public dictionary. One membership
   set governs a dict for both reading and writing: its owner, the guest editors
   the owner adds (guest_add), and members of any group attached to it
   (dict_group_add). Every artifact URL — .json/.html/.epub/.tex, the light and
   dark PDFs and the print interior — requires that same membership; nothing is
   reachable anonymously, and knowing an id or a UUID is not access. Authorised
   Publifye services may READ a dict service-to-service (lexifye is the
   ecosystem's system of record); they can never write or administer one.

9. HEADWORD CASE — TYPE IT AS YOU MEAN IT, AND DO NOT CAPITALISE BY HAND. A
   headword is STORED exactly as you write it and PRINTED with an initial capital
   when it is Latin-script: type "migdal" and the entry, the letter index, the
   EPUB nav and both PDFs all set "Migdal", while entry_get, entry_search, the
   term index and the exported JSON keep "migdal". Greek and Hebrew are never
   re-cased (a lemma is cited as written, pointing and all), and a Strong's
   headword prints in its canonical padded form (g26 → G0026). So capitalising it
   yourself buys nothing and costs you the lower-case form the fold is built on;
   inline mentions inside a definition are your prose and are never re-cased.

Definition markup grammar

The same bytes the source_syntax tool returns. The round-trip guarantee — parse(render(x)) == x — is what makes this safe to author in.

lexifye definition source — a compact, round-trippable text form for a
definition's content. Read it with definition_get_source, edit the text, push it
back with definition_set_source (or create one in a single call with
definition_add_source). Blocks are separated by a BLANK LINE; the definition id +
sha256 handle are managed by the server, never encoded in the text.

BLOCK PREFIX (the first line of a block picks its type):
  # text          heading level 1 (largest)
  ## text         heading level 2
  ### text        heading level 3 (smallest)
  - item          list — EVERY line of the block is "- item". One line is ONE
  - second item   item and may hold any mix of inline spans, so
  - with *emph*   "- with *emph* and [g:G26:agapē:ἀγάπη]" is a single bullet.
                  A line of just "- " is an empty item and keeps its slot.
  (anything else) paragraph — a line (or lines) of inline spans

INLINE SPANS (inside a paragraph, heading, or list item):
  *x*             emphasis (italic)
  **x**           bold
  [H1234]              Strong's number — Hebrew H or Greek G + 1..5 digits.
  [G3056]              CASE AND LEADING ZEROS DO NOT MATTER on input: [g26],
                       [G0026] and [G26] are the same code, and all three are
                       STORED and returned as [G26]. The same holds for the code
                       inside [h:…]/[g:…]. Only a non-code (H0, or more than 5
                       significant digits) is rejected — write \[g26] to keep a
                       code-shaped bracket as literal text.
  [h:H2617:ḥesed:חֵסֵד] Hebrew — Strong's code + transliteration + original script word
  [h:ḥesed:חֵסֵד]      Hebrew — transliteration + original script word (no code)
  [h:חֵסֵד]            Hebrew — bare original-script word (DISCOURAGED — add a translit)
  [g:G26:agapē:ἀγάπη]  Greek — code + translit + word (the [g:…] forms mirror [h:…])
  [ref:John 3:16]      Bible reference ('[1-3 ]Book Chap[:Verse[-Verse]]')
  [a:the source|https://example.com]  link — visible text | target. Only
                       http://, https:// and mailto: targets are accepted
                       (javascript:, data: and relative paths are rejected).
                       The visible text may not contain  [ ] { } | \ ( ) < >
  \X                   literal X — backslash escapes any of  \  *  [  ]  |  and a
                       leading  #  or  -  (so a paragraph can start with one)

HOUSE STYLE — TRANSLIT ALWAYS: every hebrew/greek word SHOULD carry a
transliteration. Prefer [h:CODE:translit:word]; use [h:translit:word] when you
have no Strong's code. The bare [h:word] form is still accepted, but a
translit-less span comes back with a warning nudging you to add one. When you
know the Strong's number, include it — it lets the word anchor to its lexicon
entry (and drives Darash enrichment).

When TRANSLATING, change the human-readable text but keep every span tag AND its
contents intact ([h:…], [g:…], [H1234], [ref:…] are not translatable). A link's
visible text IS translatable; its url is not.
About News Tool surface llms.txt Sign in

© 2026 Lexifye — the central dictionary service, by Publifye AS.