API reference
OpenZIM MCP exposes three kinds of MCP surfaces:
| Surface | Count (advanced mode) | Default mode |
|---|---|---|
| Tools (callable functions) | 8 | only zim_query exposed in Simple mode |
| Prompts (slash-command workflows) | 3 | Advanced mode only |
| Resources (URI-addressable data) | 1 resource + 2 templates, plus subscriptions | Advanced 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 aTextContentblock. All 8 tools are annotated-> Any, so the SDK derives nooutputSchemaand nothing arrives instructuredContent— parse the text block as JSON. - Markdown-string responses —
zim_queryrenders article bodies as a string that opens with the title as an#heading, then aPath:line and aType:line, and then a## Contentblock (when smart retrieval resolved somewhere else, aRequested Path:/Actual Path:pair replaces the singlePath:line). This rendering is reachable only throughzim_query;zim_getreturns 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 iszim_queryon its default path, and only partly: its guidance failures — no archive specified, empty query, meta-only query, chained query — return markdown withisError: false, but azim_file_pathmatching no archive returns azim_path_not_foundenvelope and any other exception returns azim_queryenvelope, both withisError: true. There is nostatuskey and nohintkey. Onlyerror,operation, andmessageare guaranteed; individual failures may merge extra self-correction keys into the same envelope (asection_not_founderror, for example, also carriesavailable_section_ids,available_section_ids_truncated,available_section_ids_total, andclosest_match). The envelope arrives as JSON text in the response’scontent— no tool advertises anoutputSchema, so nothing lands instructuredContent— andopenzim_mcp/mcp_envelope.pyrecognises it on the way out and setsisError: trueon theCallToolResult. Path entries inside errors are redacted to...filename.zimform 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_queryreturns a string on its ordinary paths, and_metais built only to render the one-line footer appended to it. So in simple mode the footer is the sole carrier: settingOPENZIM_MCP_META__FOOTER_ENABLED=falseremoves the information entirely rather than merely hiding a line. Advanced-mode tools return dicts and carry the real key.
Always present:
| Key | Meaning |
|---|---|
chars | Character count of the body |
truncated | Whether 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:
| Key | Meaning |
|---|---|
tokens_est | Estimated tokens in the body. Omitted when the tokenizer cannot initialise, deliberately, so a model can tell “zero tokens” from “no tokenizer” |
more_at_offset | The content_offset that continues from where this response stopped |
total_chars | Full length of the underlying body, of which you received chars |
suggestions | Up to 5 recovery hints as {type, value} rows (e.g. {"type": "alt_spelling", "value": "Photosynthesis"}). The footer renders the first 3 |
reason | Why the response is empty or partial — the taxonomy below |
hint | Free-text next step, on lookups whose only continuation is a larger limit rather than an offset (suggest, title search) |
detected_type / detection_confidence | Archive-type detection on metadata responses — e.g. "wikipedia" / "high" |
preset_applied | Named 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.
reason | What happened | Recovery |
|---|---|---|
0_hits | The index was searched and matched nothing | Broaden the query, or use zim_search(mode="title") for an exact title |
low_relevance | Hits came back, but none token-match the query | Same as 0_hits — treat it as a miss, not a ranking problem |
bad_query | The query could not be parsed into anything searchable | Rewrite it; punctuation-only and stop-word-only queries land here |
no_xapian_index | This archive ships no full-text index | Full-text search cannot work here at all. Use zim_search(mode="title") or zim_browse |
bad_namespace | The namespace does not exist in this archive | Call zim_metadata for the real namespace list — old and new archives use different schemes |
no_content_type_match | The content_type filter excluded everything | Drop the filter, or widen it |
sample_only | The page exhausted a sampled discovery, so done is deliberately held at false (and a next_cursor is still issued) rather than claiming completeness | Switch to zim_browse(mode="walk") for exhaustive iteration. total here is the sample size, not the namespace size |
archive_unavailable | In a cross_file fan-out, every archive failed | Infrastructural, not a query problem — check mounts and zim_health |
search_all_budget_exceeded | The aggregate cross_file timeout fired before every archive was searched | Partial results. Raise OPENZIM_MCP_SEARCH__SEARCH_ALL_TOTAL_TIMEOUT_SECONDS, or narrow the archive set |
scan_truncated | A link scan stopped before reading the whole article | Totals are a floor, not a count |
suggestion_total_is_lower_bound | A suggestion listing did not reach the end of the index | total 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.
The rendered footer#
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:
reason | Advanced footer | Simple footer |
|---|---|---|
no_xapian_index | Try `zim_search(mode='title')` or `zim_browse`. | Ask for `find article titled <title>` or `browse namespace C` — neither needs the index. |
bad_namespace | Try `zim_metadata` to see valid options. | Ask for `list namespaces` to see valid options. |
no_content_type_match | Binary 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_only | Use `zim_browse(mode='walk')` for exhaustive iteration. | Ask for `walk namespace <namespace>` for exhaustive iteration. |
archive_unavailable | Check `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.
| Parameter | Type | Default | Notes |
|---|---|---|---|
query | string | (required) | Natural-language question or instruction |
zim_file_path | string | None | Auto-selects when only one ZIM is in the allowed dirs |
limit | int | None | Max results for search/browse intents |
offset | int | 0 | Pagination offset |
content_offset | int | 0 | Pagination within long article body |
cursor | string | None | Opaque cursor for resuming paginated results |
max_content_length | int | None | Max characters for retrieved articles |
compact | bool | True | Compact prose rendering (token-budget aware) |
compact_budget | str | int | None | Override 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 |
synthesize | bool | False | When 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.
| Intent | Say something like | You get back | Default 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 searches | 3 (a cap, not a default) |
| Full-text search | “search for biology”, “find quantum entanglement” | Ranked hits | 10 |
| Cross-archive search | “search all files for python”, “search everywhere for ants” | One result block per archive | 5 per archive |
| Filtered search | “search protein in namespace C” | Hits restricted to a namespace or content type | 10 |
| Title lookup | “find article titled Photosynthesis”, “what’s the path for Ant” | The resolved entry path | 10 |
| Autocomplete | “suggestions for photo”, “autocomplete diab” | Title-prefix completions | 10 (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 body | 25 per bucket (100 if compact=False) |
| Related articles | “articles related to Climate_Change”, “what links from Ant” | Link-graph neighbours | 10 |
| 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 sidecar | 10 |
| Browse a namespace | “browse namespace C with limit 10” | A sampled page of entries | 50 |
| Walk a namespace | “walk namespace M”, “enumerate namespace C” | Deterministic cursor-paginated iteration | 200 |
| 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 thanstructureand 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>.zimandsearch <name> for <terms>(a unique basename prefix, three characters minimum, and the name must come beforefor). Every other form discards it silently — “walk namespace M in wikipedia.zim” parses as a walk ofMwith the filename dropped, which auto-selection hides while one archive is loaded and which fails with No ZIM File Specified once two are. Passzim_file_pathas 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.
andis 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):
| Key | Meaning |
|---|---|
query | The query as received |
answer_markdown | Concatenated 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_searched | Which 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_used | The 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_words | Size of the assembled answer |
considered_articles / considered_sections | Pivot 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 |
_meta | The usual envelope |
Tuning — all soft budgets under OPENZIM_MCP_SYNTHESIZE__:
| Field | Default | Effect |
|---|---|---|
top_n | 5 | Passages in the final answer |
per_archive_k | 10 | Hits taken from each archive before fusion |
output_char_budget | 4800 | Soft cap on answer_markdown (~1200 tokens). Truncates the last passage rather than dropping it |
section_affinity_threshold | 0.25 | Heading-overlap ratio before a section-attributed passage is boosted |
section_affinity_boost | 1.5 | Multiplier when it is. Deliberately conservative — it will not dominate a strong BM25 hit |
max_secondary_archive_hits | 2 | Cap 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:
| Rule | What it does |
|---|---|
| Lowercase | The whole query is lowercased. This is why topics come back lowercased, and it emits no telemetry at all |
| Misspelling map | Token-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 strip | Removes 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__:
| Field | Default | Notes |
|---|---|---|
enabled | true | false turns off all four rules on the zim_query path, lowercasing included |
misspelling_map_path | unset | Replaces the bundled map rather than extending it — a one-line file disables all 40 built-in substitutions |
misspelling_exclusion_path | unset | Tokens 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.
zim_search#
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
| Parameter | Notes |
|---|---|
query | Required 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_path | Required unless only one ZIM is in the allowed dirs. Must be omitted when cross_file=True — passing both returns an invalid_combination envelope |
cross_file | When 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_type | Optional filters, mode="fulltext" only. title and suggest have no filter step and reject either one with an invalid_combination envelope rather than answering unfiltered |
limit | 1–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 |
cursor | Accepted 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:
| Branch | Selector | Returns |
|---|---|---|
| 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 modes | entry_path="..." + view="summary" / "toc" / "structure" | Structured response (summary / TOC tree / headings) |
| Batch | entry_paths=[...] (up to 50) | {results, succeeded, failed} — per-entry success/error |
| Binary | entry_path="..." + binary=True | {path, title, mime_type, size, encoding, data} (base64) |
| Main page | main_page=True | Archive 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".
| Parameter | Notes |
|---|---|
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 |
binary | When 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_length | Per-entry char cap, min 1 (the OPENZIM_MCP_CONTENT__MAX_CONTENT_LENGTH config field has its own ≥ 100 floor) |
content_offset | Page 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 |
compact | Compact-mode prose (default False — preserves legacy byte-identical behavior; a default flip was considered and explicitly deferred for lack of adoption telemetry) |
compact_budget | Inert 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
| Parameter | Notes |
|---|---|
section_id | Required; the heading ID or anchor from a prior zim_get(view="toc") or zim_get(view="structure") response |
max_chars | Per-section char cap |
include_subsections | Default True — nested subsections are returned with the named section. Set False for just the section’s own body |
compact | Default 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_budget | Inert 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
| Mode | Behavior |
|---|---|
"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 |
| Parameter | Range | Notes |
|---|---|---|
namespace | C, 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 | |
limit | 1–200 (page) / 1–500 (walk) | Default 50 (page) / 200 (walk). page rejects anything above 200 |
cursor, offset | Both 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_assets | Browses 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"]raisesKeyError— usemetadata["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 carriesis_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 theM/Countermetadata, so you can profile an archive’s content composition without walking it. Omitted when the archive has noM/Counterentry (added in v2.1).
zim_links#
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
| Direction | Behavior |
|---|---|
"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
| Argument | Behavior |
|---|---|
| (omitted) | Combined server-state report: {health, configuration, loaded_archives, _meta}. |
zim_file_path | Per-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_changedwhen a.zimfile is added to or removed from an allowed directory — request it withresourcesListChanged: truenotifications/resources/updatedforzim://{name}when that.zimfile’s mtime or size changes — request it withresourceSubscriptions: ["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 var | Default | Notes |
|---|---|---|
OPENZIM_MCP_SUBSCRIPTIONS_ENABLED | true | master switch |
OPENZIM_MCP_WATCH_INTERVAL_SECONDS | 5 | 1–60 |
OPENZIM_MCP_RESOURCE_CACHE_TTL_SECONDS | 3600 | 0–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 call | Internal operation | Cost |
|---|---|---|
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 instead | 2 |
zim_search(mode="suggest") | suggestions | 1 |
zim_search(cross_file=True) | search (charged once per call, not per archive scanned) | 2 |
zim_get(entry_path=...) | get_entry | 1 |
zim_get(entry_paths=[...]) | get_zim_entries (per-entry charge) | N |
zim_get(binary=True) | get_binary_entry | 3 |
zim_get(view="structure") / view="toc" / view="summary" | get_structure | 1 |
zim_browse(mode="page") or zim_browse(mode="walk") | browse_namespace | 1 |
zim_metadata | get_metadata | 1 |
zim_links(direction="related") | get_related_articles | 2 |
zim_links(direction="outbound") | extract_article_links | 2 |
zim_links(direction="inbound") | get_inbound_links | 1 |
zim_health, zim_get_section, zim_query | charged 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#
operation | Cause | Recovery |
|---|---|---|
cursor_decode | Malformed, 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 code | Drop the cursor and start the sequence again |
cursor_mismatch | Issued by a different tool. Advanced surface only | Drop it; a cursor is not portable across operations |
cursor_context_mismatch | Right tool, different target — another namespace, entry, link kind, or include_assets setting | Drop it and start over for the new target; a cursor cannot be retargeted |
cursor_unsupported | zim_links(direction="related"), which returns one ranked set and does not paginate | Omit cursor entirely |
invalid_combination | Any non-empty cursor on zim_search | Use 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— baseOpenZimMcpConfigurationErrorOpenZimMcpValidationErrorOpenZimMcpArchiveErrorOpenZimMcpRateLimitError
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 call | v2 equivalent | Notes |
|---|---|---|
list_zim_files() | zim_health() → .loaded_archives | health/config/files consolidated |
get_server_health() | zim_health() → .health | health/config/files consolidated |
get_server_configuration() | zim_health() → .configuration | health/config/files consolidated |
get_zim_metadata(path) | zim_metadata(path) → .metadata | now includes namespace breakdown too |
list_namespaces(path) | zim_metadata(path) → .namespaces | now 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.