Resources, prompts & subscriptions
OpenZIM MCP exposes three MCP “resources” (URI-addressable data), three slash-command “prompts” (pre-built workflows), and a polling-based subscription system for live update notifications. This page is the canonical reference for each.
Source of truth: openzim_mcp/tools/resource_tools.py, openzim_mcp/tools/prompts.py, openzim_mcp/subscriptions.py.
Resources, prompts, and subscriptions are always available regardless of tool mode (Simple or Advanced).
Notation: code samples on this page use MCP JSON-RPC tool-call framing (
{"name": "...", "arguments": {...}}). Your MCP client handles the wire framing; you supply the tool name and argument shape.
Resources
MCP resources are URI-addressable data. Clients with a resource browser or @-mention picker (Claude Code, Inspector) surface them automatically.
zim://files
JSON list of every ZIM file in the allowed directories.
[
{
"name": "wikipedia_en_100_2026-02.zim",
"path": "/srv/zim/wikipedia_en_100_2026-02.zim",
"size": 124857600,
"modified": "2026-02-15T10:30:00"
}
]
Same shape as the loaded_archives field returned by the zim_health tool.
zim://{name}
Overview of one ZIM file. {name} is the bare basename without .zim (e.g. wikipedia_en_100_2026-02). Returns metadata, namespace summary, and a main-page preview (truncated to 2000 chars).
{
"name": "wikipedia_en_100_2026-02",
"path": "/srv/zim/wikipedia_en_100_2026-02.zim",
"metadata": {
"Title": "Wikipedia",
"Language": "eng",
"Creator": "Wikipedia",
"Flavour": "maxi"
},
"namespaces": { },
"main_page_preview": "..."
}
If a section fails to load (rare — corrupt archive, missing metadata), it’s reported in metadata_error / namespaces_error / main_page_error rather than aborting the whole response.
zim://{name}/entry/{path}
Single entry served with native MIME type. The MIME type is detected from libzim’s Item.mimetype and reported back per request — text entries return text bodies; binary entries (images, PDFs, audio) return raw bytes (FastMCP base64-wraps).
URL encoding requirement
Clients MUST URL-encode / as %2F in the {path} segment. FastMCP’s URI template engine treats / as a segment separator, so a literal slash won’t route. Other RFC 3986 reserved characters in the path also need encoding (e.g. ? as %3F).
zim://wikipedia_en/entry/A%2FClimate_change
zim://wikipedia_en/entry/I%2FFlag_of_France.svg
Without the encoding, the resource fetch returns a “not found” error even when the entry exists.
MIME type behavior
- HTML / text →
text/html,text/plain,application/json,application/xml,application/javascriptreturned as decoded text. - Binary (anything else) → returned as raw bytes; FastMCP base64-wraps them on the wire.
- Unknown / missing MIME →
application/octet-stream.
Charset parameters (text/html; charset=utf-8) are stripped before reporting; only the bare MIME type ends up in the response.
When to use this resource vs the zim_get tool
| Concern | Per-entry resource | zim_get tool |
|---|---|---|
| Direct browser/MCP-client rendering (HTML, image, PDF) | preferred | not designed for this |
| Native MIME type | yes | wraps article body in markdown envelope |
| Smart-retrieval fallback | no — direct path only, must know exact path | yes — search-derived term fallback |
Truncation / max_content_length | no | yes |
| Binary content with metadata wrapping | bare bytes only | use zim_get(binary=True) for the {path, title, mime_type, size, encoding, data} envelope |
For LLM workflows that need processed text + smart fallback, prefer the tool. For “render this entry” workflows, prefer the resource.
Prompts
MCP prompts are pre-built workflows clients can invoke as slash commands. Each one returns a list of messages instructing the LLM to chain a specific multi-step ZIM operation against the 8-tool advanced surface.
Hardening: user-supplied arguments are sanitized before interpolation — control characters replaced with spaces, backticks stripped (template delimiter), length capped at 200 characters. Apostrophes and double quotes are preserved (real entry paths contain them, e.g.
C/Schrödinger's_cat). If args reduce to empty after sanitization, the prompt body asks the user to re-supply them.
/research
Signature: research(topic: str).
Searches across every ZIM file for a topic, then drills into the top hits. Workflow:
- Dispatch
zim_searchwithcross_file=Trueto find which ZIM files have relevant content. - For each top hit, dispatch
zim_getwithview="summary"for a concise overview. - Identify sub-topics worth exploring; ask the user which to pursue.
Step 1 call shape:
{
"name": "zim_search",
"arguments": {
"query": "<topic>",
"cross_file": true,
"limit": 10
}
}
Step 2 call shape (per hit):
{
"name": "zim_get",
"arguments": {
"zim_file_path": "<from step 1 result>",
"entry_path": "<from step 1 result>",
"view": "summary"
}
}
Use case: “I want to research X but I don’t know which archive has the best material.”
/summarize
Signature: summarize(zim_file_path: str, entry_path: str).
Three-part article summary. Workflow:
zim_getwithview="toc"for the structural overview.zim_getwithview="summary"for the lead-paragraph summary.zim_linkswithdirection="outbound"for the most-mentioned related entries.
Combined into: (a) one-paragraph TL;DR, (b) section list, (c) 5–10 most relevant outbound links.
Step 1 call shape:
{
"name": "zim_get",
"arguments": {
"zim_file_path": "<arg>",
"entry_path": "<arg>",
"view": "toc"
}
}
Step 2 call shape:
{
"name": "zim_get",
"arguments": {
"zim_file_path": "<arg>",
"entry_path": "<arg>",
"view": "summary"
}
}
Step 3 call shape:
{
"name": "zim_links",
"arguments": {
"zim_file_path": "<arg>",
"entry_path": "<arg>",
"direction": "outbound"
}
}
/explore
Signature: explore(zim_file_path: str).
High-level briefing of one ZIM file. Workflow:
zim_metadata— title, language, creator, flavour, plus the deterministic namespace breakdown (surfaces minority namespaces — M, W, X, I).zim_getwithmain_page=True— the entry point.zim_browsewithmode="walk",namespace="C",limit=5— sample article content.
Step 1 call shape:
{
"name": "zim_metadata",
"arguments": {
"zim_file_path": "<arg>"
}
}
Step 2 call shape:
{
"name": "zim_get",
"arguments": {
"zim_file_path": "<arg>",
"main_page": true
}
}
Step 3 call shape:
{
"name": "zim_browse",
"arguments": {
"zim_file_path": "<arg>",
"namespace": "C",
"mode": "walk",
"limit": 5
}
}
Produces a compact briefing of what the archive is, what it covers, and what typical content looks like.
When to use prompts vs raw tool calls
If your client supports MCP prompts, prefer them — they save the LLM from having to remember the orchestration. If you’re building your own client or your host doesn’t surface prompts, the same workflows are easy to call directly with the underlying tools.
Subscriptions
OpenZIM MCP supports the MCP resources/subscribe and resources/unsubscribe requests on zim://files and zim://{name}. The server emits notifications/resources/updated whenever:
- A
.zimfile is added to or removed from an allowed directory (subscription target:zim://files) - A specific
.zimfile’s mtime changes (subscription target:zim://{name})
Detection is mtime-based — same-size archive replacement (which is common when re-downloading from Kiwix) is detected via the mtime change.
Configuration
| Env var | Default | Range | Notes |
|---|---|---|---|
OPENZIM_MCP_SUBSCRIPTIONS_ENABLED | true | bool | master switch — false skips the watcher entirely; subscribe calls succeed but never fire updates |
OPENZIM_MCP_WATCH_INTERVAL_SECONDS | 5 | 1-60 | polling interval; the watcher rescans allowed directories on this cadence |
For production HTTP services with low-priority watching, increase to 15-30s to reduce I/O churn. For interactive desktop use, the default is fine.
Lifespan integration
Under the streamable-HTTP transport the watcher is started/stopped via a lifespan-context wrapper around FastMCP’s own lifespan handler. (FastMCP supplies a custom lifespan, so add_event_handler('startup', …) is silently a no-op — the wrapper is the only path that works.)
Subscriber behavior
- The fan-out is concurrent (
asyncio.gather); one slow subscriber doesn’t delay the others. - Per-subscriber send timeout is 5 seconds (
TIMEOUTS.SUBSCRIPTION_SEND_SECONDS); a subscriber that doesn’t respond is treated as dead and evicted from the registry. asyncio.CancelledErrorfrom a subscriber is re-raised, not swallowed — clean shutdown propagates correctly.
Client example (pseudocode)
After the initial MCP handshake, subscribe via your client SDK’s resource-subscription API. The wire-level interaction is:
{
"jsonrpc": "2.0",
"id": 1,
"method": "resources/subscribe",
"params": { "uri": "zim://files" }
}
The server then pushes notifications when changes are detected:
{
"jsonrpc": "2.0",
"method": "notifications/resources/updated",
"params": { "uri": "zim://files" }
}
On each notification, re-read the resource (resources/read with the same URI) to refresh your local view. Specifics depend on your MCP client SDK — check its subscription API for the idiomatic wrapper.
Implementation note
Resource handler registration uses a private FastMCP attribute (_mcp_server); the SDK doesn’t yet expose a stable public hook for handler-level interception. This is fragile if FastMCP changes its internals; the project tracks the risk and will migrate once a stable public API lands upstream.
API reference for tools: API reference. HTTP transport details: HTTP and Docker Deployment. Configuration knobs: Configuration.
v1.x is in maintenance through 2026-11-27. See CHANGELOG for the v1 → v2 migration table.