API reference

OpenZIM MCP exposes three kinds of MCP surfaces:

SurfaceCount (advanced mode)Default mode
Tools (callable functions)8only zim_query exposed in Simple mode
Prompts (slash-command workflows)3Advanced mode only
Resources (URI-addressable data)1 resource + 2 templates, plus subscriptionsAdvanced mode only (change notifications additionally require the HTTP transport)

In Simple mode (the default) only the zim_query natural-language tool is exposed. Pass --mode advanced (or set OPENZIM_MCP_TOOL_MODE=advanced) to expose all 8 specialized tools below.

v2.0.0 collapsed the prior 22-tool advanced surface into 8 consolidated tools. The full mechanical v1 → v2 mapping is reproduced in the migration table at the bottom of this page; it also lives in CHANGELOG.md.

Source of truth: openzim_mcp/tools/. If signatures here disagree with code, file an issue — code is canonical.

Output format#

Tools return one of:

  • JSON payload responses (most tools) — typed payloads (SearchResponse, ArchiveMetadataResponse, ServerHealthResponse, etc.) serialized as JSON text inside a TextContent block. All 8 tools are annotated -> Any, so the SDK derives no outputSchema and nothing arrives in structuredContent — parse the text block as JSON.
  • Markdown-string responses — zim_query renders article bodies as a string that opens with the title as an # heading, then a Path: line and a Type: line, and then a ## Content block (when smart retrieval resolved somewhere else, a Requested Path: / Actual Path: pair replaces the single Path: line). This rendering is reachable only through zim_query; zim_get returns the JSON payload described under its own entry.
  • Tool errors — every tool catches exceptions and returns a structured ToolErrorPayload ({error: true, operation, message, context?}) rather than raising. The exception is zim_query on its default path, and only partly: its guidance failures — no archive specified, empty query, meta-only query, chained query — return markdown with isError: false, but a zim_file_path matching no archive returns a zim_path_not_found envelope and any other exception returns a zim_query envelope, both with isError: true. There is no status key and no hint key. Only error, operation, and message are guaranteed; individual failures may merge extra self-correction keys into the same envelope (a section_not_found error, for example, also carries available_section_ids, available_section_ids_truncated, available_section_ids_total, and closest_match). The envelope arrives as JSON text in the response’s content — no tool advertises an outputSchema, so nothing lands in structuredContent — and openzim_mcp/mcp_envelope.py recognises it on the way out and sets isError: true on the CallToolResult. Path entries inside errors are redacted to ...filename.zim form so the canonical allowed-directory layout is never leaked.

The _meta envelope#

Every dict-returning response carries a _meta key. It is the machine-readable answer to “why is this response empty, partial, or smaller than I expected”.

Simple mode does not give you the key. zim_query returns a string on its ordinary paths, and _meta is built only to render the one-line footer appended to it. So in simple mode the footer is the sole carrier: setting OPENZIM_MCP_META__FOOTER_ENABLED=false removes the information entirely rather than merely hiding a line. Advanced-mode tools return dicts and carry the real key.

Always present:

KeyMeaning
charsCharacter count of the body
truncatedWhether the body was cut to fit a cap. Always present, false when nothing was cut — do not test for the key’s presence

Present when they apply:

KeyMeaning
tokens_estEstimated tokens in the body. Omitted when the tokenizer cannot initialise, deliberately, so a model can tell “zero tokens” from “no tokenizer”
more_at_offsetThe content_offset that continues from where this response stopped
total_charsFull length of the underlying body, of which you received chars
suggestionsUp to 5 recovery hints as {type, value} rows (e.g. {"type": "alt_spelling", "value": "Photosynthesis"}). The footer renders the first 3
reasonWhy the response is empty or partial — the taxonomy below
hintFree-text next step, on lookups whose only continuation is a larger limit rather than an offset (suggest, title search)
detected_type / detection_confidenceArchive-type detection on metadata responses — e.g. "wikipedia" / "high"
preset_appliedNamed archive preset that shaped a search or summary response

reason values and what to do about them#

A reason means the call succeeded and the answer is thin. Retrying the identical call will return the identical thin answer — each value implies a different next move.

reasonWhat happenedRecovery
0_hitsThe index was searched and matched nothingBroaden the query, or use zim_search(mode="title") for an exact title
low_relevanceHits came back, but none token-match the querySame as 0_hits — treat it as a miss, not a ranking problem
bad_queryThe query could not be parsed into anything searchableRewrite it; punctuation-only and stop-word-only queries land here
no_xapian_indexThis archive ships no full-text indexFull-text search cannot work here at all. Use zim_search(mode="title") or zim_browse
bad_namespaceThe namespace does not exist in this archiveCall zim_metadata for the real namespace list — old and new archives use different schemes
no_content_type_matchThe content_type filter excluded everythingDrop the filter, or widen it
sample_onlyThe page exhausted a sampled discovery, so done is deliberately held at false (and a next_cursor is still issued) rather than claiming completenessSwitch to zim_browse(mode="walk") for exhaustive iteration. total here is the sample size, not the namespace size
archive_unavailableIn a cross_file fan-out, every archive failedInfrastructural, not a query problem — check mounts and zim_health
search_all_budget_exceededThe aggregate cross_file timeout fired before every archive was searchedPartial results. Raise OPENZIM_MCP_SEARCH__SEARCH_ALL_TOTAL_TIMEOUT_SECONDS, or narrow the archive set
scan_truncatedA link scan stopped before reading the whole articleTotals are a floor, not a count
suggestion_total_is_lower_boundA suggestion listing did not reach the end of the indextotal undercounts; raise limit

The Recovery column is written for advanced mode, where every tool it names is registered. In simple mode zim_query is the only tool there is, so the same reason renders the equivalent natural-language request instead — see the rendered footer below.

The last three are the ones worth special-casing in client code: sample_only, scan_truncated and suggestion_total_is_lower_bound all ride responses that otherwise look complete — sample_only even keeps done: false on a page that already returned everything the sample held. The first nine also shape the footer; the last two only appear in _meta.

When OPENZIM_MCP_META__FOOTER_ENABLED is true (the default), zim_query responses get a one-line markdown blockquote built from _meta and appended to the body — a token-budget summary normally, or the recovery hint when a reason is set. It also requires compact=True, which is the simple-mode default.

The recovery hint’s text depends on OPENZIM_MCP_TOOL_MODE. zim_query is registered in both modes, so a hint written for the advanced tool set would otherwise reach simple-mode clients that cannot call any of it. The advanced hint names the tool; the simple hint is a request the intent parser resolves:

reasonAdvanced footerSimple footer
no_xapian_indexTry `zim_search(mode='title')` or `zim_browse`.Ask for `find article titled <title>` or `browse namespace C` — neither needs the index.
bad_namespaceTry `zim_metadata` to see valid options.Ask for `list namespaces` to see valid options.
no_content_type_matchBinary assets (images, media) aren't in the search index — use `zim_browse` (include_assets=True) to discover them, or drop the `content_type` filter.Binary assets (images, media) aren't in the search index — ask again without the `content_type` filter. This tool cannot discover assets, but `get binary content of <path>` fetches one you can already name.
sample_onlyUse `zim_browse(mode='walk')` for exhaustive iteration.Ask for `walk namespace <namespace>` for exhaustive iteration.
archive_unavailableCheck `zim_health` and server logs.That is a server-side failure, not the query — retry shortly, or ask an operator to check the logs.

The same split applies to every other recovery string the server emits: the cross_file no-results body, the “entry not found” and metadata-key-miss messages, and the (Image content - Cannot display directly; ...) line, which names zim_get(entry_path=..., binary=True) in advanced mode and get binary content of <path> in simple. zim_health() in the error-envelope troubleshooting steps likewise becomes list available ZIM files.

Setting it to false suppresses the line. On an advanced-mode dict response that costs you nothing, because _meta is a real key there; in simple mode it is the only channel, so the information is gone.


Simple mode#

zim_query#

Single natural-language tool exposed by default. Routes to the underlying advanced operations via an intent parser.

Signature:

zim_query(
    query: str,
    zim_file_path: Optional[str] = None,
    limit: Optional[int] = None,
    offset: int = 0,
    content_offset: int = 0,
    cursor: Optional[str] = None,
    max_content_length: Optional[int] = None,
    compact: bool = True,
    compact_budget: Optional[Union[str, int]] = None,
    synthesize: bool = False,
) -> Any

At runtime the tool returns a string for ordinary calls (markdown for every intent except list available ZIM files, which embeds a JSON array in the string), a SynthesizeResponse dict when synthesize=True, and a ToolErrorPayload dict when it rejects an argument, hits the rate limit, cannot decode a cursor, or the synthesize pipeline fails. The -> Any annotation is deliberate — it keeps the SDK from deriving an outputSchema.

ParameterTypeDefaultNotes
querystring(required)Natural-language question or instruction
zim_file_pathstringNoneAuto-selects when only one ZIM is in the allowed dirs
limitintNoneMax results for search/browse intents
offsetint0Pagination offset
content_offsetint0Pagination within long article body
cursorstringNoneOpaque cursor for resuming paginated results
max_content_lengthintNoneMax characters for retrieved articles
compactboolTrueCompact prose rendering (token-budget aware)
compact_budgetstr | intNoneOverride the compact-mode budget. Named profiles: tiny (2000), small (4000), medium (6000), large (12000). Integers are clamped to 500–64000. An unrecognised string silently falls back to medium
synthesizeboolFalseWhen True, return a SynthesizeResponse (multi-source briefing)

Recognized phrasings#

In simple mode zim_query is the entire tool surface, so the phrasings it recognizes are the API. There are 21 of them. The parser scores every pattern and dispatches the most specific match, so wording matters more than it looks — the traps are called out below the table.

IntentSay something likeYou get backDefault limit
List archives“list available ZIM files”, “show archives”Every loaded archive, as JSON embedded in the string—
Main page“show main page”, “home page”The archive’s front page—
List namespaces“list namespaces”, “what namespaces”The archive’s namespace breakdown—
Archive metadata“metadata for wikipedia.zim”, “details about this archive”Title, language, creator, index capabilities, counts—
Fetch an article“get article Evolution”, “show the page Photosynthesis”The rendered article body—
Topic lookup“tell me about photosynthesis”, “everything about ants”Auto-fetches on a strong title match, otherwise searches3 (a cap, not a default)
Full-text search“search for biology”, “find quantum entanglement”Ranked hits10
Cross-archive search“search all files for python”, “search everywhere for ants”One result block per archive5 per archive
Filtered search“search protein in namespace C”Hits restricted to a namespace or content type10
Title lookup“find article titled Photosynthesis”, “what’s the path for Ant”The resolved entry path10
Autocomplete“suggestions for photo”, “autocomplete diab”Title-prefix completions10 (hard max 50)
Section outline“show structure of Biology”, “outline of Protein”Headings and section anchors—
Table of contents“table of contents for Evolution”, “toc of Ant”The hierarchical TOC—
Summary“summary of Quantum_mechanics”, “summarize Protein”Opening-paragraph summary—
One section“the Biochemistry section of Protein”, “section Synthesis of Protein”That section’s body—
Outbound links“links in Ant”, “references from Protein”Links extracted from the body25 per bucket (100 if compact=False)
Related articles“articles related to Climate_Change”, “what links from Ant”Link-graph neighbours10
Inbound links“what links to Ant”, “backlinks for Protein”, “which articles link to Evolution”The articles that link to this one, from the link-graph sidecar10
Browse a namespace“browse namespace C with limit 10”A sampled page of entries50
Walk a namespace“walk namespace M”, “enumerate namespace C”Deterministic cursor-paginated iteration200
Batch fetch“get articles C/Ant, C/Bee”Several entries in one call. The values must be path-shaped — bare titles reach the intent but extract nothing and return “Missing Entry Paths”—
Binary content“get image Logo.png”, “download pdf Manual.pdf”, “raw content for File.pdf”Base64 payload with its native MIME type—

Phrasings that trip people up:

  • Metadata needs the literal word. “metadata for X” and “details about X.zim” match; “tell me about X.zim” does not — it routes to a topic lookup and tries to fetch an article named after the file.
  • A section needs one of three shapes: section <name> of <path>, the <name> section of <path>, or <path> section <name> as a trailing suffix. Anything else scores lower than structure and you get the outline instead of the section.
  • “related” is overloaded. “links in X” gives outbound links from the body; “articles related to X” gives link-graph neighbours. “related to X” alone lands on the latter.
  • The binary verb must sit next to the noun. “get image Logo.png” works; “get the image Logo.png” does not — the article breaks the pattern and the query falls through to full-text search. That adjacency rule is not special to binary: a determiner or quantifier between verb and noun drops most patterns to a literal search at confidence 0.50. “list all zim files”, “list the namespaces”, “walk the namespace A”, “browse the A namespace” and “show homepage” (one word) all search for their own wording.
  • Two patterns outrank the one you meant. Browsing scores 0.85 and article fetch 0.80, so “show article Evolution” becomes a namespace browse, discards the title, and answers Missing or Invalid Namespace. Separately, the bare word “contents” is a table-of-contents trigger, so “show contents of namespace C” becomes a TOC lookup for an article called “namespace c”.
  • Only two phrasings read an archive name out of the query: metadata for <file>.zim and search <name> for <terms> (a unique basename prefix, three characters minimum, and the name must come before for). Every other form discards it silently — “walk namespace M in wikipedia.zim” parses as a walk of M with the filename dropped, which auto-selection hides while one archive is loaded and which fails with No ZIM File Specified once two are. Pass zim_file_path as an argument instead of naming it in the sentence.
  • Chain detection is driven by a verb list, not by the word “then”. “search for X, then get article Y” returns Chained Operations Detected guidance; “then fetch Y” is not recognised and runs as one literal search. and is not a connector at all: “search for climate and get article Climate crisis” runs the fetch and drops the search.

Pagination differs per intent: suggestions for, find article titled and articles related to do not page at all — raise limit rather than passing offset. Every response also ends with <!-- intent=<name> cert=<0.NN> -->, which is the quickest way to see what a surprising answer actually dispatched to; a cert of 0.50 almost always means no pattern matched and your wording was searched literally.

Returns: intent-specific output — a markdown list for searches, a rendered article for retrievals, a SynthesizeResponse when synthesize=True. On the default (non-synthesize) path, the guidance failures — no archive specified, empty query, meta-only query, chained query — come back as markdown rather than an error envelope, so they carry isError: false. A zim_file_path that matches no loaded archive is not one of them: it returns a zim_path_not_found envelope, and any other exception the catch-all absorbs returns a zim_query envelope. Both set isError: true. The synthesize=True path instead refuses bad paths, meta-only queries, and chained queries with a ToolErrorPayload, so those do set isError: true.

synthesize=True — cross-archive briefings#

zim_query(synthesize=True, ...) is a different operation wearing the same tool name. Instead of routing to one underlying call, it runs a retrieval pipeline across every allowed archive and returns a cited briefing. It is the only cross-archive answering path in the product, and the only rich capability a simple-mode client can reach.

Read this before using it: answer_markdown is not generated prose. It is the selected source passages concatenated verbatim, each followed by an inline [cite: <cite_id>] marker. Nothing is paraphrased, summarised or written by a model — the server has no model. Treat it as a pre-assembled evidence bundle to hand your own LLM, not as an answer to display.

The pipeline: per-archive search → RRF fusion across archives → passage extraction → optional cross-encoder rerank (only with the [reranker] extra) → section attribution and heading-affinity boost → scraper output sunk below articles, except the translation hub a query ending in a language asks for (see zim_search below) → budget enforcement → answer rendering → citation building.

Arguments this mode reads, and the ones it refuses. limit selects the number of cited passages (SynthesizeConfig.top_n), so it is capped at 50 here rather than at the 1,000 the search path allows — a larger value is refused with invalid_limit rather than clamped. offset, content_offset and cursor are refused with invalid_combination: the pipeline fuses a single ranked set across every archive and has no next page. Pass synthesize=False to page a search instead.

Response (SynthesizeResponse):

KeyMeaning
queryThe query as received
answer_markdownConcatenated source passages with [cite: …] markers
passages{cite_id, text_markdown, rank, score} per passage. Dropped in compact mode, which moves rank/score onto the citations instead
citations{cite_id, archive, entry_path, title, section_id, section_title, rank, score} — entry_path is what you pass to zim_get
archives_searchedWhich archives actually contributed
archives_failed{archive, error} per archive that could not be opened. Present only when at least one failed — this is the only cross-archive answering path, so its coverage claim is auditable rather than implied. A failure degrades the briefing rather than refusing it: the answer is still built from the archives that did open
fallback_usedThe fusion stage that ordered the hits: rrf_fusion (multiple archives) or xapian_score (single archive, or fusion unavailable). Not an error, and not a reranker signal — the cross-encoder runs after fusion and never changes this field, so the schema’s third literal reranker is never emitted
total_chars / total_wordsSize of the assembled answer
considered_articles / considered_sectionsPivot handles for a follow-up turn without re-running the search: the other top hits minus the top passage’s own article, and that article’s other sections. Only the top passage’s article and section are excluded, so lower-ranked cited articles still appear here. Empty when there was no candidate space
_metaThe usual envelope

Tuning — all soft budgets under OPENZIM_MCP_SYNTHESIZE__:

FieldDefaultEffect
top_n5Passages in the final answer
per_archive_k10Hits taken from each archive before fusion
output_char_budget4800Soft cap on answer_markdown (~1200 tokens). Truncates the last passage rather than dropping it
section_affinity_threshold0.25Heading-overlap ratio before a section-attributed passage is boosted
section_affinity_boost1.5Multiplier when it is. Deliberately conservative — it will not dominate a strong BM25 hit
max_secondary_archive_hits2Cap per non-primary archive, bounding cross-archive flooding. 0 restricts to the primary archive

Unlike the default path, this one does return error envelopes: bad paths, meta-only queries and chained queries are refused with isError: true rather than answered with markdown guidance.


Query rewriting#

Before intent matching, zim_query rewrites the text you sent. The rewrite is silent — nothing in the response says it happened — so it is worth knowing what it changes.

Three normalisations always run, and are not covered by the kill switch below: whitespace is collapsed, a leaked limit=10 / offset=5 fragment is stripped out of the prose, and trailing politeness (please, thanks) is removed.

Then four rules run in order:

RuleWhat it does
LowercaseThe whole query is lowercased. This is why topics come back lowercased, and it emits no telemetry at all
Misspelling mapToken-wise substitution from a bundled 40-entry map, retried after stripping surrounding punctuation and a possessive 's. It is probe-gated: a token that resolves to a real title in the archive is left alone
Stopword stripRemoves one leading the, a, an or of. Also probe-gated — the article is kept when the full query resolves to a real title
X of Y decomposition<attr> of <entity> and <entity>'s <attr> are split so the entity drives the lookup

The misspelling rule changes which article you get, with no note in the response: on a Wikipedia archive, photosythesis retrieves the artificial-photosynthesis article and the reply contains no trace of the original spelling. The probe gate is archive-scoped, so the same query can rewrite differently depending on how many archives are loaded — with two loaded and none pinned, the probe cannot run and the rule fires unconditionally.

Three configuration fields control it, under OPENZIM_MCP_QUERY_REWRITE__:

FieldDefaultNotes
enabledtruefalse turns off all four rules on the zim_query path, lowercasing included
misspelling_map_pathunsetReplaces the bundled map rather than extending it — a one-line file disables all 40 built-in substitutions
misspelling_exclusion_pathunsetTokens never substituted. The bundled list is empty

Either path pointing at a missing file fails at startup with a pydantic validation error naming the resolved path, rather than falling back silently.

One gap to know about: enabled=false does not reach zim_search(mode="title"), which calls the misspelling and stopword rules directly without consulting the configuration. Title-mode search always rewrites, applies only those two rules, and never probes first — though it does echo the rewritten string back in the response’s query field, so the change is at least observable there.

Advanced tools (8)#

Every tool on this surface — in both modes — is published with the MCP annotations readOnlyHint: true and openWorldHint: false. Nothing here writes to an archive and nothing reaches the network, so a client that gates auto-approval on readOnlyHint can run the whole surface without prompting, and one that caches on openWorldHint can treat a repeated call as replayable. destructiveHint and idempotentHint are deliberately not published: the MCP spec defines both as meaningful only when readOnlyHint is false.

Full-text / title / suggest search dispatch. Collapses five v1 search tools (search_zim_file, search_all, search_with_filters, find_entry_by_title, get_search_suggestions) into one.

zim_search(
    query: str,
    mode: Literal["fulltext", "title", "suggest"] = "fulltext",
    zim_file_path: Optional[str] = None,
    cross_file: bool = False,
    namespace: Optional[str] = None,
    content_type: Optional[str] = None,
    limit: Optional[int] = None,
    offset: int = 0,
    cursor: Optional[str] = None,
) -> Any
ParameterNotes
queryRequired search term, title, or partial-query prefix (depending on mode)
mode"fulltext" (default; libzim full-text index), "title" (title-indexed lookup with fast C/<Title> path), or "suggest" (auto-complete prefix)
zim_file_pathRequired unless only one ZIM is in the allowed dirs. Must be omitted when cross_file=True — passing both returns an invalid_combination envelope
cross_fileWhen True, queries every allowed ZIM file. The SearchAllResponse carries one row per file in results; each row’s result is that file’s SearchResponse
namespace, content_typeOptional filters, mode="fulltext" only. title and suggest have no filter step and reject either one with an invalid_combination envelope rather than answering unfiltered
limit1–1000 for plain single-archive fulltext; 1–100 when namespace/content_type filters are set; 1–50 for cross_file=True and for title/suggest
cursorAccepted for surface uniformity but rejected when non-empty (invalid_combination envelope) — paginate single-archive fulltext with offset instead

Returns: mode-shaped response — SearchResponse / SearchAllResponse / SearchWithFiltersResponse / FindEntryResponse / SearchSuggestionsResponse — or ToolErrorPayload on validation failure. Every next_cursor in these responses is nulled; page with offset.

mode="title" (pinned or cross_file=True) and mode="suggest" sink scraper output the archive files as its own entry — per-language translation hubs (/languages/…), image-caption stubs (/ency/imagepages/…) and subtitle sidecars (.srt) — below real articles, even when the archive ranks it higher. The demote reorders the page it is handed: nothing is dropped, so total and the paging arithmetic are unaffected, and a small limit whose page holds no real article still leads with scraper output. Title rows therefore come back in presentation order, not score order. Read results order as authoritative and treat score as a match-quality signal rather than a rank; re-sorting rows by score undoes the demote and puts the scraper output back on top.

mode="fulltext" sinks the same scraper output below real articles — single-archive, filtered, and within each archive’s block when cross_file=True — and zim_query search intents and synthesize=True do too, after any cross-encoder rerank, so the optional reranker cannot lift a translation hub back above the article it translates. Fulltext search rows carry no score, so there is nothing to re-sort on. synthesize=True passages, and compact-mode citations, do carry a score, but the demote reorders them without rescoring, so scraper output can sit below an article with a lower score. Read their order, which rank is renumbered to match, as authoritative; re-sorting them by score can put the scraper output back on top. The reorder happens within each page: total, next_offset and every offset are unchanged, an article on page two is never pulled forward onto page one, and scraper output on a later page stays there.

A translation the query asks for is not demoted. On every surface above — title, suggest, fulltext, the zim_query routes and synthesize=True — a query that asks for a topic’s translation leaves that topic’s own hub, /languages/<topic>.html, where the archive (or the optional reranker) ranked it. A query asks for one when it ends with one or more language names — MedlinePlus’s languages, such as spanish, chinese or haitian creole, and the qualifiers simplified, traditional, mandarin, cantonese, persian and filipino; english does not count — optionally followed by the word language or languages, and optionally preceded by in and the, with a topic before them: asthma in spanish, asthma spanish, anemia in the chinese language and alzheimer's disease chinese each keep their topic’s hub in place. The hub is matched on its path, with the topic lowercased and stripped to letters and digits (alzheimersdisease), and quotes around a term are ignored wherever they fall. Only a trailing language counts: japanese encephalitis and german measles ask for nothing, and neither does in chinese alone, nor sign language, whose trailing noun names no language. Query syntax is read as topic text, not parsed, so asthma AND chinese asks for a hub no archive has and exempts nothing. The hub is not promoted — an article the archive ranked above it stays above it — and everything else still sinks for that query: image-caption stubs, subtitle sidecars, other topics’ hubs and the per-language portals such as /languages/french.html.

Single-archive fulltext responses carry a top-level zim_file_path naming the archive that answered — including when the server auto-selected it because only one is loaded. Hand it straight to zim_get, zim_get_section, zim_links, zim_browse or zim_metadata, all of which require the field. (mode="title" states the same fact per hit, as zim_file; the cross-archive fan-out states it per file.) If you get a zim_file_path: Field required rejection anyway, it names the archive for you whenever exactly one is loaded.


zim_get#

Single-entry / batch / binary / main-page / view-mode entry fetch. Collapses seven v1 retrieval tools into one.

zim_get(
    zim_file_path: str,
    entry_path: Optional[str] = None,
    entry_paths: Optional[List[str]] = None,
    view: Literal["full", "summary", "toc", "structure"] = "full",
    binary: bool = False,
    main_page: bool = False,
    max_content_length: Optional[int] = None,
    content_offset: int = 0,
    compact: bool = False,
    compact_budget: Optional[Union[str, int]] = None,
) -> Any

Exactly one of these four branch selectors must be set:

BranchSelectorReturns
Single-entry (article body)entry_path="..."EntryResponse dict — {path, title, content, _meta} plus content_type, requested_path, content_offset and total_chars when they apply
Single-entry view modesentry_path="..." + view="summary" / "toc" / "structure"Structured response (summary / TOC tree / headings)
Batchentry_paths=[...] (up to 50){results, succeeded, failed} — per-entry success/error
Binaryentry_path="..." + binary=True{path, title, mime_type, size, encoding, data} (base64)
Main pagemain_page=TrueArchive main page entry

The four branches are mutually exclusive. Setting more than one (e.g. entry_path + entry_paths, or main_page + view="summary") returns a ToolErrorPayload with operation="invalid_path_combination".

ParameterNotes
view"full" (default; article body), "summary" (opening paragraph), "toc" (hierarchical TOC), "structure" (headings + section anchors). Combining a non-"full" view with binary=True or main_page=True is rejected with operation="invalid_path_combination", not silently ignored
binaryWhen True, returns raw bytes (base64) with native MIME type. Default per-entry cap 10,000,000 bytes (10 MB decimal, not 10 MiB)
max_content_lengthPer-entry char cap, min 1 (the OPENZIM_MCP_CONTENT__MAX_CONTENT_LENGTH config field has its own ≥ 100 floor)
content_offsetPage through long articles without re-fetching the prefix. Honored on a single-entry view="full" call only — batch, binary, main-page and the non-"full" views have no second page, so a non-zero value there is rejected with operation="invalid_path_combination" rather than dropped
compactCompact-mode prose (default False — preserves legacy byte-identical behavior; a default flip was considered and explicitly deferred for lack of adoption telemetry)
compact_budgetInert on this tool. It is declared in the signature but never forwarded, so it does not cap the response. Only zim_query honors it

Smart retrieval: if direct path access fails, single-entry mode falls back through a five-step ladder (see Smart retrieval); resolved paths are cached. When fallback resolves to a different path, the response carries a requested_path key alongside path.


zim_get_section#

Section-level fetch by section ID. Renamed from the v1 get_section tool; the compact=True default is the notable behavioral change.

zim_get_section(
    zim_file_path: str,
    entry_path: str,
    section_id: str,
    max_chars: Optional[int] = None,
    include_subsections: bool = True,
    compact: bool = True,
    compact_budget: Optional[Union[str, int]] = None,
) -> Any
ParameterNotes
section_idRequired; the heading ID or anchor from a prior zim_get(view="toc") or zim_get(view="structure") response
max_charsPer-section char cap
include_subsectionsDefault True — nested subsections are returned with the named section. Set False for just the section’s own body
compactDefault True (compact rendering — [Table N: …] placeholders instead of full tables). Since v2.4.0, compact=False returns the true raw section body with full tables
compact_budgetInert on this tool — declared but never forwarded. Only zim_query honors it

Returns: {entry_path, title, section_id, section_title, level, parent_id, content_markdown, char_count, word_count, truncated, _meta} — the section body plus its heading metadata, with a narrow_widened_to_first_child flag added when include_subsections=False had to widen into the first child. There are no next/previous-section keys; walk the outline from zim_get(view="toc") instead.


zim_browse#

Namespace browse / walk dispatch. Collapses the v1 browse_namespace + walk_namespace tools.

zim_browse(
    zim_file_path: str,
    namespace: str,
    mode: Literal["page", "walk"] = "page",
    cursor: Optional[str] = None,
    limit: Optional[int] = None,
    offset: int = 0,
    include_assets: bool = False,
) -> Any
ModeBehavior
"page" (default)Sampled namespace overview, paginated by limit + offset. For very large namespaces may cap entries — use mode="walk" for exhaustive iteration
"walk"Cursor-paginated deterministic iteration by entry ID. Pair next_cursor with a follow-up call until done: true
ParameterRangeNotes
namespaceC, M, W, X on modern archives; A, I, - on legacy ones. Which you get depends on the archive — see ZIM concepts and read the real breakdown from zim_metadata
limit1–200 (page) / 1–500 (walk)Default 50 (page) / 200 (walk). page rejects anything above 200
cursor, offsetBoth modes emit and accept a cursor. offset is page-only: a walk advances by cursor, so mode="walk" rejects a non-zero one sent on its own (invalid_combination). Sent alongside a walk cursor it is accepted and ignored, per the precedence rule under Cursors
include_assetsBrowses of C-namespace pages on domain-scheme archives hide css/js/font assets by default in both modes; pass True to include them. Only the page_info.assets_filtered flag is mode="page"-only

Returns: a paginated envelope for both modes — {results, next_cursor, total, done, page_info, namespace, …} — with next_cursor driving walk iteration until done: true.


zim_metadata#

Combined archive metadata + namespaces. Collapses the v1 get_zim_metadata + list_namespaces tools — the response now includes both the M-namespace metadata and the deterministic namespace breakdown.

zim_metadata(zim_file_path: str) -> Any

Returns: structured response with:

  • metadata — archive M-namespace fields, keyed exactly as libzim emits them, which is capitalized: Title, Language, Creator, Publisher, Date, Flavour, and so on. metadata["title"] raises KeyError — use metadata["Title"].
  • namespaces — a namespace breakdown. Archives above 1,000 entries are sampled rather than walked, so the per-namespace counts outside the dominant namespace are projections, not exact totals (each bucket carries is_authoritative). The sample is a fixed stride over entry ids, seeded from the archive UUID: the same archive always answers the same thing, and minority namespaces (M, W, X, I, -) are surfaced rather than lost to whichever entries a random draw happened to hit.
  • archive_identity — {uuid, is_multipart}, the libzim archive identity (added in v2.1).
  • index_capabilities — {has_fulltext_index, has_title_index}: whether full-text search and title suggestions will work against this archive (added in v2.1).
  • counter_breakdown — {mimetype: count} parsed from the M/Counter metadata, so you can profile an archive’s content composition without walking it. Omitted when the archive has no M/Counter entry (added in v2.1).

Outbound / related link-graph dispatch. Collapses the v1 extract_article_links + get_related_articles tools.

zim_links(
    zim_file_path: str,
    entry_path: str,
    direction: Literal["outbound", "inbound", "related"] = "outbound",
    kind: Literal["internal", "external", "media"] = "internal",
    cursor: Optional[str] = None,
    limit: Optional[int] = None,
    offset: int = 0,
) -> Any
DirectionBehavior
"outbound" (default)Links extracted from the article body — one kind per call ("internal" default; pass kind="external" or kind="media" for the other buckets; category_totals in the response counts four buckets — those three plus anchor, which no kind can fetch). Drops non-navigable schemes (javascript:, mailto:, tel:, data:, blob:, vbscript:)
"inbound"Pages that link TO this entry, ranked by linker importance. Requires a pre-built link-graph sidecar (see below)
"related"Outbound link-graph neighbors with deduplication

cursor / offset apply to "outbound" and "inbound"; "related" returns a single ranked set and rejects both (cursor_unsupported for a cursor, invalid_combination for a non-zero offset). kind buckets an article’s own outbound links, so "inbound" and "related" reject a non-default kind with invalid_combination instead of returning an unbucketed set under the label you asked for.

direction="inbound" requires the link-graph sidecar to be built first: openzim-mcp build link-graph <archive>.zim walks the archive once and writes <archive>.zim.linkgraph.sqlite next to it (--force overwrites an existing sidecar; --output PATH relocates it). When the sidecar is absent or stale, zim_links returns a structured inbound_sidecar_unavailable error rather than failing. A sidecar is stale when the archive’s UUID no longer matches the one recorded at build time (it was rebuilt or replaced), or when its schema version predates the running server’s.

3.0.0 invalidates every sidecar built by 2.x. Edge targets are now stored under the path the archive can actually serve rather than the raw percent-encoded href, so the schema version was bumped and older sidecars are rejected on load. Rebuild each one with openzim-mcp build link-graph --force <archive>.zim; --force is required because the old file is still sitting next to the archive. Until then direction="inbound" returns inbound_sidecar_unavailable, and no other direction is affected.

Relative hrefs are resolved against the source entry’s directory; redirects are followed to resolved paths; the content namespace is identified correctly on domain-scheme archives; self-referential refs are rejected.

On a scraped archive (warc2zim/ZIMIT), whose entry paths are host-prefixed, the site’s own links are commonly stored as absolute URLs — https://www.iep.utm.edu/aris-log/ inside iep.utm.edu/aristotle/. Those count as internal, with a resolved path, whenever the archive can actually serve them: the host is matched against the archive’s own with and without a leading www., and the candidate path is verified (and redirect-followed) before the row moves. A same-host URL the archive never captured stays external — a path that fetches nothing would be worse than an honest external row. On mwoffliner archives, where the first path segment is a namespace letter rather than a host, nothing is promoted.

Returns: a paginated {results, next_cursor, total, done, page_info, …} envelope for every direction. direction="outbound" adds kind and category_totals alongside; "inbound" and "related" carry neither, and key the article as entry_path rather than path/title.

Outbound entries carry url/text/title (plus domain for external, alt for media). url is the raw href as written in the article — it does not necessarily round-trip into zim_get. Internal rows additionally carry path, the resolved entry path, and that is the one to pass to zim_get. Inbound entries are {path, title, inbound_degree, anchor_text}; related entries are {path, title, …} rows.

Inbound rows are ranked by the linker’s inbound_degree, except that site furniture sinks to the end: a linker whose inbound_degree is at least half the sidecar’s node count — on a sidecar of 50 or more nodes — is linked from nearly every page — it is the site’s navigation — and would otherwise lead most answers. Nothing is dropped, so total and paging are unaffected. Read results order as authoritative; re-sorting rows on inbound_degree puts the navigation pages back on top.


zim_health#

Two calls in one. With no argument, returns combined server health, configuration, and loaded archives (collapses the v1 get_server_health + get_server_configuration + list_zim_files tools). With a zim_file_path, validates and diagnoses that one archive instead (added in v2.1).

zim_health(zim_file_path: Optional[str] = None) -> Any
ArgumentBehavior
(omitted)Combined server-state report: {health, configuration, loaded_archives, _meta}.
zim_file_pathPer-archive integrity/identity check via libzim — runs Archive.check() and reports checksum, index capabilities, and identity. Lets a caller tell a valid archive from a corrupt one.

Server-state response (no argument) — shape (abbreviated):

{
  "health": {
    "timestamp": "2026-05-27T15:30:00.000000+00:00",
    "status": "healthy",
    "server_name": "openzim-mcp",
    "uptime_info": { "process_id": "[REDACTED]", "started_at": "..." },
    "cache_performance": { "hits": 1024, "misses": 256, "hit_rate": 0.8 },
    "health_checks": { "directories_accessible": 1, "zim_files_found": 5, "permissions_ok": true },
    "recommendations": ["Server is running optimally"],
    "warnings": []
  },
  "configuration": {
    "configuration": {
      "server_name": "openzim-mcp",
      "allowed_directories": ["...zim-files"],
      "allowed_directories_count": 1,
      "cache_enabled": true,
      "cache_max_size": 100,
      "config_hash": "<sha256>",
      "server_pid": "[REDACTED]"
    },
    "diagnostics": { "validation_status": "ok", "warnings": [], "recommendations": [] },
    "timestamp": "2026-05-27T15:30:00.000000+00:00"
  },
  "loaded_archives": [
    { "name": "wikipedia_en_100_2026-02.zim", "path": "...wikipedia_en_100_2026-02.zim", "size": "119.07 MB", "size_bytes": 124857600, "modified": "2026-02-15T10:30:00" }
  ]
}

Over the HTTP/SSE transports process_id / server_pid are "[REDACTED]" and allowed directories are shown as ...basename — diagnostic output frequently lands in bug reports. Over the local stdio transport these report the real PID and full paths, matching the unredacted loaded_archives[].path values that clients pass back to other tools.

The three blocks are built independently. If the health or configuration builder raises, that block alone is replaced by a ToolErrorPayload — {"error": true, "operation": "get server health" | "get server configuration", "message": ..., "context": ...}, the only two operation values in the codebase that are not snake_case — and the rest of the response, loaded_archives included, is still returned. The top-level dict is not an error envelope, so isError stays false; check .health and .configuration for an error key before reading their fields. A top-level envelope comes back only two ways — a rate-limit rejection (operation: "rate_limited"), or an exception escaping the tool body (operation: "zim_health"), which for the no-argument call means only the directory scan behind loaded_archives, since the other two builders swallow their own exceptions, and for the zim_file_path call below means any validation failure.

Archive-validation response (with zim_file_path, added in v2.1):

{
  "is_valid": true,
  "has_checksum": true,
  "checksum": "<hex>",
  "has_fulltext_index": true,
  "has_title_index": true,
  "uuid": "<archive uuid>",
  "is_multipart": false,
  "path": "...archive.zim",
  "name": "archive.zim",
  "has_link_graph": true,
  "link_graph": {
    "built_at": "2026-09-04T16:05:54Z",
    "builder_version": "<openzim-mcp version that built it>",
    "schema_version": "3",
    "node_count": 15990,
    "edge_count": 422722,
    "is_stale": false
  }
}

is_valid is the result of libzim’s Archive.check() structural-integrity probe — a quick way to tell a valid archive from a corrupt or truncated one. A non-indexed archive reports has_fulltext_index: false; full-text zim_search against it then degrades gracefully to a no_xapian_index reason instead of erroring.

has_link_graph answers, without a trial call, whether zim_links(direction="inbound") can be served for this archive — previously the only way to find out was to issue one and read the failure. When a sidecar is present, link_graph carries its own meta table alongside it. Read is_stale, not just presence: a sidecar built for a different archive revision or an older schema is on disk but refuses every inbound query, and rebuilding it is openzim-mcp build link-graph <archive> --force. has_link_graph: false means no sidecar file at all.


MCP prompts#

Three slash-command workflows. See openzim_mcp/tools/prompts.py.

User-supplied arguments are sanitized: ASCII control characters are replaced with spaces, backticks are stripped (template delimiter), and the value is capped at 200 characters before being interpolated. Apostrophes and double quotes are preserved (real entry paths contain them, e.g. C/Schrödinger's_cat).

/research#

research(topic: str)

Workflow: zim_search(query=topic, cross_file=True) across archives, then zim_get(entry_path=..., view="summary") on the top hits, then ask the user which thread to pursue.

/summarize#

summarize(zim_file_path: str, entry_path: str)

Workflow: zim_get(view="toc") → zim_get(view="summary") → zim_links(direction="outbound"), combined into a TL;DR + section list + 5–10 most relevant outbound links.

/explore#

explore(zim_file_path: str)

Workflow: zim_metadata → zim_get(main_page=True) → zim_browse(namespace="C", mode="walk", limit=5). Produces a compact briefing.

If a prompt is invoked without required args (or args reduce to empty after sanitization), the response asks the user to supply them.


MCP resources#

One concrete resource (zim://files, listed by resources/list) plus two URI templates (listed by resources/templates/list). See openzim_mcp/tools/resource_tools.py.

zim://files#

JSON list of every ZIM file in the allowed directories. Same shape as the loaded_archives field of zim_health.

zim://{name}#

Overview of one ZIM file: metadata, namespace breakdown, and main-page preview (truncated to 2000 characters). {name} is the bare basename without .zim (e.g. wikipedia_en_climate_change_mini_2024-06).

zim://{name}/entry/{path}#

Single entry served with native MIME type:

  • HTML / text entries → text/html, text/plain, application/json, etc., body as text.
  • Binary entries (images, PDFs) → appropriate MIME, body as raw bytes (the SDK base64-wraps).

Encoding requirement: clients MUST URL-encode / as %2F in the {path} segment because the SDK’s URI template engine treats / as a segment separator. Example:

zim://wikipedia_en/entry/A%2FClimate_change

A literal slash will fail to route. See the Resources, prompts & subscriptions guide for full details.


Resource subscriptions#

Clients open one long-lived subscriptions/listen stream naming the notification kinds they want (resources/subscribe was removed by the 2026-07-28 revision). Under the HTTP transport (--transport http) the server then publishes:

  • notifications/resources/list_changed when a .zim file is added to or removed from an allowed directory — request it with resourcesListChanged: true
  • notifications/resources/updated for zim://{name} when that .zim file’s mtime or size changes — request it with resourceSubscriptions: ["zim://{name}"]

subscriptions/listen is served only where the file watcher can run — the HTTP transport with subscriptions enabled. On stdio and SSE the capability is not advertised and a listen request fails with method-not-found instead of acknowledging a stream that would stay silent.

Configuration:

Env varDefaultNotes
OPENZIM_MCP_SUBSCRIPTIONS_ENABLEDtruemaster switch
OPENZIM_MCP_WATCH_INTERVAL_SECONDS51–60
OPENZIM_MCP_RESOURCE_CACHE_TTL_SECONDS36000–86400; read TTL for zim://{name} overviews (entry reads keep the watcher-bounded TTL), 0 disables

See Resources, prompts & subscriptions for full client-side examples.


Rate limiting#

All tools are subject to a global token-bucket limiter (default 20 work units/s, burst 40 — most operations cost 1 unit, searches 2, binary fetches 3). Costs are charged per internal operation, not per tool call — a v2 tool that dispatches over multiple modes resolves to a specific underlying operation key:

Tool callInternal operationCost
zim_search(mode="fulltext") or zim_search(mode="title")search / find_entry_by_title — a fulltext call carrying namespace or content_type keys on search_with_filters instead2
zim_search(mode="suggest")suggestions1
zim_search(cross_file=True)search (charged once per call, not per archive scanned)2
zim_get(entry_path=...)get_entry1
zim_get(entry_paths=[...])get_zim_entries (per-entry charge)N
zim_get(binary=True)get_binary_entry3
zim_get(view="structure") / view="toc" / view="summary"get_structure1
zim_browse(mode="page") or zim_browse(mode="walk")browse_namespace1
zim_metadataget_metadata1
zim_links(direction="related")get_related_articles2
zim_links(direction="outbound")extract_article_links2
zim_links(direction="inbound")get_inbound_links1
zim_health, zim_get_section, zim_querycharged under their own tool names (zim_query once per call)1

Tune via OPENZIM_MCP_RATE_LIMIT__REQUESTS_PER_SECOND, OPENZIM_MCP_RATE_LIMIT__BURST_SIZE, and OPENZIM_MCP_RATE_LIMIT__PER_OPERATION_LIMITS. See Configuration.

When the limit is exceeded the tool returns a ToolErrorPayload with operation: "rate_limited" instead of raising — and since 2.6.0 that envelope reaches the client as a CallToolResult with isError: true (see openzim_mcp/mcp_envelope.py). There is no retry_after field. The wait is stated in prose in message ("Rate limit exceeded for operation 'search'. Please wait 1.23 seconds before retrying.") and repeated in machine-readable form in context, which is always present on this error and formatted operation=<internal op>, cost=<N>, wait_time=<S>s.


Cursors#

A next_cursor is an opaque resume handle. “Opaque” is the contract — do not parse it — but four properties govern whether a call using one will work.

Who issues and who accepts them. Three tools emit a cursor: zim_browse (both modes), zim_links with direction="outbound", and zim_query for its browse and walk intents. Four accept one: those three plus zim_search, which accepts the parameter and rejects every non-empty value with invalid_combination — it never emits one either, and nulls any the data layer mints. Paginate search with offset.

A cursor is bound to a tool, an archive and a context. It carries the issuing tool’s name, a hash of the archive’s validated path (not its contents, so two identical copies at different paths have different identities), and the context fields its tool needs — namespace and include_assets for browse, entry path and link kind for links. Change any of those and the call is refused rather than silently answering about something else. Cursors are interchangeable in one direction: between zim_query and the advanced tool implementing the same operation, which mint identical cursors.

Precedence is not symmetric. An explicit limit wins over the page size the cursor encodes — the encoded size is only a fallback when you omit limit. An explicit offset, by contrast, loses: when a cursor is present its offset is used and yours is ignored. So pass a cursor or an offset, never both. This holds on every cursor-accepting surface, including the ones that refuse a bare offset (below) — zim_browse(mode="walk") resumes from the cursor and ignores the offset riding along with it, exactly as mode="page" does.

There is a length cap, on one surface. zim_query rejects a cursor over 2048 characters with a cursor_decode envelope. The advanced tools have no length check.

When a cursor is refused#

operationCauseRecovery
cursor_decodeMalformed, truncated, wrong version, or over zim_query’s 2048-character cap. Also zim_query’s catch-all — it reports every cursor fault under this one codeDrop the cursor and start the sequence again
cursor_mismatchIssued by a different tool. Advanced surface onlyDrop it; a cursor is not portable across operations
cursor_context_mismatchRight tool, different target — another namespace, entry, link kind, or include_assets settingDrop it and start over for the new target; a cursor cannot be retargeted
cursor_unsupportedzim_links(direction="related"), which returns one ranked set and does not paginateOmit cursor entirely
invalid_combinationAny non-empty cursor on zim_searchUse offset — single-archive mode="fulltext" only

“Drop the cursor and pass offset” is the usual recovery, but it does not work everywhere. zim_search accepts offset only for single-archive mode="fulltext"; title, suggest and cross_file=True reject it and have no pagination at all. zim_browse(mode="walk") rejects a bare one with invalid_combination — a walk has no offset to seek to, so iteration must be driven by its cursor — and zim_links(direction="related") rejects one outright, since it returns a single ranked set that only limit resizes. (Both used to accept the argument and drop it, handing back page one to a caller who believed it had advanced.) Where the substitution does work, the cursor page and the equivalent offset page are the same rows.

Error responses#

Every tool wraps exceptions and returns a structured ToolErrorPayload — except zim_query’s default path, which returns markdown guidance for handler-side failures — delivered as JSON text in the response’s content — never in structuredContent, since no tool advertises an outputSchema — with isError: true set on the CallToolResult. error (always true), operation, and message are always present; context is added only when the tool supplies one. Three paths merge operation-specific self-correction keys: the section_not_found envelope returned by zim_get_section also carries available_section_ids (capped at 50), available_section_ids_truncated, and available_section_ids_total, plus closest_match when a near-miss section ID is found; an unknown_argument envelope carries unknown_arguments and accepted_arguments, plus closest_matches when a stray name is close to a real one; and an invalid_argument envelope carries invalid_arguments:

{
  "error": true,
  "operation": "invalid_path_combination",
  "message": "`entry_path` and `entry_paths` are mutually exclusive."
}

The error classes in openzim_mcp/exceptions.py are the canonical source for the underlying exception hierarchy:

  • OpenZimMcpError — base
  • OpenZimMcpConfigurationError
  • OpenZimMcpValidationError
  • OpenZimMcpArchiveError
  • OpenZimMcpRateLimitError

Absolute filesystem paths in error messages are redacted to ...filename.zim form on all transports. PIDs and paths in zim_health diagnostics are redacted over the HTTP/SSE transports and shown in full over local stdio. Error text is safe to copy into bug reports.


v1 → v2 migration#

The full mechanical mapping. Every v1 tool name in this table is intentional — it is the canonical place to look up “what does my old call become?”. For the narrative context see CHANGELOG.md → migration table.

v1 callv2 equivalentNotes
list_zim_files()zim_health() → .loaded_archiveshealth/config/files consolidated
get_server_health()zim_health() → .healthhealth/config/files consolidated
get_server_configuration()zim_health() → .configurationhealth/config/files consolidated
get_zim_metadata(path)zim_metadata(path) → .metadatanow includes namespace breakdown too
list_namespaces(path)zim_metadata(path) → .namespacesnow includes namespace breakdown too
get_main_page(path)zim_get(path, main_page=True)one of four mutually-exclusive branches
search_zim_file(path, q)zim_search(q, zim_file_path=path)default mode="fulltext"
search_all(q)zim_search(q, cross_file=True)one row per file in results, each with a nested result
search_with_filters(path, q, ns=, ct=)zim_search(q, zim_file_path=path, namespace=ns, content_type=ct)filters only meaningful for fulltext
find_entry_by_title(path, title)zim_search(title, zim_file_path=path, mode="title")fast title-indexed C/<Title> path
get_search_suggestions(path, prefix)zim_search(prefix, zim_file_path=path, mode="suggest")autocomplete-style prefix
get_zim_entry(path, entry_path)zim_get(path, entry_path=entry_path)smart-retrieval fallback on miss
get_zim_entries(path, entries)zim_get(path, entry_paths=entries)up to 50 per call; per-entry cost
get_binary_entry(path, entry_path)zim_get(path, entry_path=entry_path, binary=True)base64 wire payload, 10,000,000-byte default cap
get_entry_summary(path, entry_path)zim_get(path, entry_path=entry_path, view="summary")one of four view modes
get_table_of_contents(path, entry_path)zim_get(path, entry_path=entry_path, view="toc")one of four view modes
get_article_structure(path, entry_path)zim_get(path, entry_path=entry_path, view="structure")one of four view modes
get_section(path, entry_path, section_id)zim_get_section(path, entry_path, section_id)now defaults compact=True
browse_namespace(path, namespace)zim_browse(path, namespace)default mode="page"
walk_namespace(path, namespace)zim_browse(path, namespace, mode="walk")cursor-paginated deterministic iteration
extract_article_links(path, entry_path)zim_links(path, entry_path)default direction="outbound"
get_related_articles(path, entry_path)zim_links(path, entry_path, direction="related")replaces standalone tool

There are no on-the-wire aliases at v2.0 — old tool names disappear cleanly per the foundational v2 decisions.


Need configuration help? See Configuration. Deploying over HTTP? See HTTP and Docker Deployment. Using resources / subscriptions? See Resources, prompts & subscriptions.

The v1.x maintenance window closed when v2.5.0 shipped (2026-06-18); only the current major line is supported — see SECURITY.md for the policy. The CHANGELOG carries the v1 → v2 migration table and the v3.0.0 breaking-changes entry.

Documentation for v3.3.4 · Edit this page on GitHub ↗