Skip to main content
Glama

🆕 NEW in v1.1.0: Structured tool output! All 17 JSON-returning tools now emit MCP structuredContent alongside the legacy text envelope — no more double-stringified JSON, no more escape soup. Plus a major namespace-handling fix for new-scheme archives (list_namespaces / browse_namespace / walk_namespace were silently broken on Wikipedia-style ZIMs), pagination for extract_article_links, case-insensitive find_entry_by_title with proper scoring, and CORS support for browser MCP clients. Learn more →

Still on the v1.0.0 highlights? Streamable HTTP transport, batch entry retrieval, and per-entry resources are documented in the v1.0.0 section.

Dual Mode Support: Choose between Simple mode (1 intelligent natural language tool, default) or Advanced mode (21 specialized tools, plus 3 MCP prompts and 3 MCP resources) to match your LLM's capabilities.

Built for LLM Intelligence

OpenZIM MCP transforms static ZIM archives into dynamic knowledge engines for Large Language Models. Unlike basic file readers, this tool provides intelligent, structured access that LLMs need to effectively navigate and understand vast knowledge repositories.

Why LLMs Love OpenZIM MCP:

  • Smart Navigation: Browse by namespace (articles, metadata, media) instead of blind searching

  • Context-Aware Discovery: Get article structure, relationships, and metadata for deeper understanding

  • Intelligent Search: Advanced filtering, auto-complete suggestions, and relevance-ranked results

  • Performance Optimized: Cached operations and pagination prevent timeouts on massive archives

  • Relationship Mapping: Extract internal/external links to understand content connections

Whether you're building a research assistant, knowledge chatbot, or content analysis system, OpenZIM MCP gives your LLM the structured access patterns it needs to unlock the full potential of offline knowledge archives. No more fumbling through raw text dumps!

OpenZIM MCP is a modern, secure, and high-performance MCP (Model Context Protocol) server that enables AI models to access and search ZIM format knowledge bases offline.

ZIM (Zeno IMproved) is an open file format developed by the openZIM project, designed specifically for offline storage and access to website content. The format supports high compression rates using Zstandard compression (default since 2021) and enables fast full-text searching, making it ideal for storing entire Wikipedia content and other large reference materials in relatively compact files. The openZIM project is sponsored by Wikimedia CH and supported by the Wikimedia Foundation, ensuring the format's continued development and adoption for offline knowledge access, especially in environments without reliable internet connectivity.

Related MCP server: ZIM MCP Server

Features

  • Dual Mode Support: Choose between Simple mode (1 intelligent natural language tool, default) or Advanced mode (21 specialized tools)

  • Streamable HTTP Transport: 🆕 Run as a long-running service over HTTP — bearer-token auth, CORS, health endpoints, multi-arch Docker image, and resource subscriptions

  • Batch Entry Retrieval: 🆕 Fetch up to 50 entries per call with get_zim_entries — pairs naturally with HTTP, where round-trip cost matters

  • Per-Entry MCP Resources: 🆕 Stream individual entries via zim://{name}/entry/{path} with native MIME types — browse HTML, PDFs, and images directly

  • Resource Subscriptions: 🆕 Clients subscribe to zim://files and zim://{name} and receive notifications/resources/updated when archives change

  • Multi-Archive Search: Search every ZIM file at once with search_all — no need to know which archive holds the answer

  • MCP Prompts: Pre-built workflow slash commands (/research, /summarize, /explore) that orchestrate multi-step ZIM operations

  • Find Entries by Title: Resolve titles to entry paths instantly with find_entry_by_title — case-insensitive, optionally cross-file

  • Binary Content Retrieval: Extract PDFs, images, videos, and other embedded media for multi-agent workflows

  • Security First: Comprehensive input validation and path traversal protection

  • High Performance: Intelligent caching and optimized ZIM file operations

  • Smart Retrieval: Automatic fallback from direct access to search-based retrieval for reliable entry access

  • Well Tested: 80%+ test coverage with comprehensive test suite

  • Modern Architecture: Modular design with dependency injection

  • Type Safe: Full type annotations throughout the codebase

  • Configurable: Flexible configuration with validation

  • Observable: Structured logging and health monitoring

What's new in v1.1.0

Structured tool output

The 17 JSON-returning tools now emit MCP structuredContent alongside the legacy content[].text envelope. Old clients keep parsing the text JSON; new clients read the dict directly. The biggest beneficiary is search_all, whose per_file[].result field used to be a pre-rendered markdown blob escaped twice through json.dumps — it's now a real nested dict.

The four prose/markdown tools (search_zim_file, search_with_filters, get_zim_entry, get_main_page) and the simple-mode zim_query stay on -> str by design.

Namespace handling, fixed

In new-scheme ZIM archives (the modern format used by current Kiwix Wikipedia builds), libzim's iterable surface only exposes the C namespace and reaches metadata through archive.metadata_keys. The previous code parsed the first character of each entry path as the namespace, so Evolution looked like namespace 'E', Bob_Dylan like 'B', favicon.png like 'F' — including emoji buckets like '🐜'. search_with_filters(namespace='C') was silently dropping ~95% of legitimate hits.

list_namespaces, browse_namespace, walk_namespace, and the namespace= filter now branch on archive.has_new_namespace_scheme: new-scheme C uses entry_count as an authoritative total, M is enumerated from metadata_keys, W is surfaced via has_main_entry / has_illustration. Old-scheme archives are unaffected.

Pagination for extract_article_links

extract_article_links previously dumped every internal/external/media link in one call. On a heavily-linked Wikipedia article (~6k links) that was ~400 KB and overflowed the response token budget. The tool now accepts limit / offset / kind parameters; full counts ship in total_internal_links / total_external_links / total_media_links so callers can size the next page. Parsed extraction is cached once per entry and sliced in-memory (~40× speedup on cached pages).

Smarter find_entry_by_title

The fast path was case-sensitive, so "evolution" against an archive titled "Evolution" missed and fell through to suggestion fallback with a hardcoded score: 0.8. Now the fast path tries five case variants × C/A namespaces; suggestion results get rank-derived scores in (0, 0.95] so an exact case-insensitive match (promoted to 1.0) always outranks partials.

Per-entry resource size caps

zim://{name}/entry/{path} now caps text bodies at 256 KB UTF-8 with a notice pointing at get_zim_entry for paged reads. Oversize binary bodies are refused (not silently clipped, since a sliced PDF/PNG won't open) — callers should use get_binary_entry, which has explicit max_size_bytes and a truncated flag.

Simple mode actually works

_register_simple_tools was also calling _register_advanced_tools, so simple-mode clients received every advanced tool's schema in the prompt anyway — defeating the entire point of the mode and inflating prefill into the multi-thousand-token range. Fixed: simple mode now registers exactly one tool (zim_query). Confirmed against llama.cpp's MCP webui — single-turn prompt size dropped from ~6,200 tokens to ~1,100.

CORS for browser MCP clients

Two additions to the HTTP transport's CORS layer:

  • MCP-Protocol-Version is now in allow_headers. Browser MCP clients send this on every post-init request per the MCP spec; without it, the second preflight returned 400 Disallowed CORS headers and the connection dropped.

  • DELETE is now in allow_methods. The MCP streamable-HTTP SDK uses DELETE for explicit session termination.

Polish

  • get_server_health now reports a real started_at and uptime_seconds instead of "unknown".

  • Configuration redaction format changed from the misleading ...data to unambiguous <redacted>/data.

  • Server-tools timestamps go through a single _utc_now_iso() helper so a response no longer mixes timezone-aware UTC with naive local.

  • zim_query("") rejects empty input upfront with example queries instead of falling through to a no-op search.

  • get_search_suggestions schema now documents the 2-character minimum.

  • The "cache hit rate is low" warning waits until ≥50 accesses before commenting (previously fired at 22% during normal session warm-up).

  • get_zim_entry's truncation tail now reads "of body content" so callers can tell the limit applies to the body, not the wrapper headers.

What's new in v1.0.0

Streamable HTTP transport

Run OpenZIM MCP as a long-running service. Pass --transport http (or set OPENZIM_MCP_TRANSPORT=http) and the server boots a Starlette app on 127.0.0.1:8000 by default with:

  • Bearer-token auth — set OPENZIM_MCP_AUTH_TOKEN; comparison is timing-safe and the attempted token is never logged.

  • Safe-default startup check — the server refuses to bind a non-localhost host without a token. (Bind 127.0.0.1 for local-only access; put a reverse proxy in front for TLS.)

  • CORS allow-list — explicit origins via OPENZIM_MCP_CORS_ORIGINS; wildcard * is rejected at startup.

  • Health endpoints — /healthz (liveness) and /readyz (at least one allowed dir is readable). Both exempt from auth so probes work cleanly.

  • Multi-arch Docker image — ghcr.io/cameronrye/openzim-mcp:1.1.2, builds for linux/amd64 and linux/arm64, runs as non-root.

Legacy SSE transport is also available via --transport sse (or OPENZIM_MCP_TRANSPORT=sse) for clients that haven't migrated to streamable-HTTP. SSE does not apply the bearer-token / CORS / health-endpoint middleware, so the server refuses to start with --transport sse bound to anything other than 127.0.0.1/::1/localhost. For exposed deployments use --transport http.

Batch entry retrieval

get_zim_entries fetches up to 50 entries in one call. Per-entry failures don't abort the batch — each result includes its index from the input order plus either content (success) or error (failure). Different zim_file_path values are allowed in one batch, so a multi-archive workflow can fan out from a single search. Single-archive batches can pass bare path strings paired with a top-level zim_file_path default, so the call site stays flat instead of dict-heavy.

Per-entry MCP resources

zim://{name}/entry/{path} exposes individual entries with their native MIME type:

  • HTML and text entries return text bodies (text/html, text/plain, application/json, ...).

  • Binary entries (images, PDFs) return raw bytes (FastMCP base64-wraps them).

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. Example: zim://wikipedia_en/entry/C%2FClimate_change. (This is a constraint of the current mcp[cli] SDK.)

Resource subscriptions

Subscribe to zim://files or zim://{name} and the server emits notifications/resources/updated whenever the directory contents change or a .zim file is replaced. Polling interval is configurable (OPENZIM_MCP_WATCH_INTERVAL_SECONDS, default 5 s) and the feature can be disabled with OPENZIM_MCP_SUBSCRIPTIONS_ENABLED=false. Implementation note: this depends on a private FastMCP attribute (_mcp_server) for handler registration.

Polish & fixes

Smarter archive handling

  • get_related_articles resolves relative hrefs against the source entry's directory and identifies the content namespace correctly on domain-scheme archives (previously returned nothing).

  • Suggestion fallback uses SuggestionSearcher(archive).suggest(text) (the prior archive.suggest() call didn't exist).

  • list_zim_files gains a case-insensitive name_filter substring argument; one shared cache slot regardless of filter value.

  • search_zim_file accepts an opaque cursor parameter; passing the cursor alone resumes pagination without restating the query.

Cleaner content extraction

  • Heading-id resolution falls through id → mw-headline anchor → preceding <a name=""> → slug, returning (id, source) so consumers can distinguish real anchors from synthetic slugs.

  • Summary extraction skips USWDS banners and skip-nav blocks above the first <h1> (MedlinePlus / NIH / NIST style sites).

  • Link extraction drops non-navigable schemes (javascript:, mailto:, tel:, data:, blob:, vbscript:).

  • Per-entry paths sanitized in get_zim_entries.

Server hygiene

  • __version__ reads from importlib.metadata; serverInfo.version reports openzim-mcp's actual version (no longer the FastMCP SDK default).

  • HTTP transport's subscription watcher starts via wrapped lifespan.

  • Per-entry zim:// returns libzim's native MIME (was returning a placeholder).

Streamlined scope

v1.0.0 reduces the advanced-mode tool surface from 27 to 21 by removing administrative/inspection helpers that didn't pull their weight: warm_cache, cache_stats, cache_clear, get_random_entry, diagnose_server_state, and resolve_server_conflicts. The cache itself remains; the explicit management tools were dropped. Multi-instance conflict tracking was removed entirely — instance_tracker.py is gone — which means HTTP server instances coexist freely without configuration warnings.

Review pass

End-to-end review pass before tagging: tightened path/PID redaction in error and diagnostics responses, locked OPTIONS /mcp behind auth, fixed cache poisoning on transient libzim errors, resolved redirects before rendering with cycle detection, preserved Unicode in heading slugs (Arabic, Chinese, Cyrillic, Japanese), made rate-limiting atomic, and split zim_operations.py into a zim/ package via mixin classes.

What's new in v0.9.0

search_all queries every ZIM file in your allowed directories at once and merges the results — no need to know which archive holds the answer.

MCP Prompts

Three pre-built workflows you can invoke as slash commands in MCP-aware clients:

  • /research <topic> — search across all archives, then drill into top hits

  • /summarize <zim_file_path> <entry_path> — TOC + summary + key links

  • /explore <zim_file_path> — high-level briefing of a ZIM's contents

Find entries by title

find_entry_by_title resolves a title (or partial title) to one or more entry paths, with case-insensitive matching. Cheaper than full-text search when you already know the article name.

Power-user tools

  • walk_namespace — deterministic cursor-paginated namespace iteration (vs. browse_namespace which samples)

  • get_related_articles — outbound link-graph neighbours of a given entry

MCP Resources

First use of the MCP resources primitive — your client's resource browser and @-mention picker now see ZIM files directly:

  • zim://files — index of all available ZIM files

  • zim://{name} — overview of one ZIM (metadata, namespaces, main page preview)

  • zim://{name}/entry/{path} (new in 1.0.0) — single entry served with native MIME type (clients must URL-encode / as %2F in the path segment)

Reliability fixes

  • Namespace listing now deterministically surfaces minority namespaces (M, W, X, I) that random sampling could miss

  • Search filtering uses streaming scan instead of a hard 1000-hit cap (rare-mime-type filters now return matches that were previously hidden)

  • Error messages route by failure mode first (no more "check disk space" for "entry not found")

Quick Start

Installation

# Install from PyPI as an isolated CLI tool (recommended)
uv tool install openzim-mcp

# Or install into your current environment with pip
pip install openzim-mcp

Development Installation

For contributors and developers:

# Clone the repository
git clone https://github.com/cameronrye/openzim-mcp.git
cd openzim-mcp

# Install dependencies
uv sync

# Install development dependencies
uv sync --dev

Prepare ZIM Files

Download ZIM files (e.g., Wikipedia, Wiktionary, etc.) from the Kiwix Library and place them in a directory:

mkdir ~/zim-files
# Download ZIM files to ~/zim-files/

Running the Server

# Simple mode (default) - 1 intelligent natural language tool
openzim-mcp /path/to/zim/files
python -m openzim_mcp /path/to/zim/files

# Advanced mode - all 21 specialized tools
openzim-mcp --mode advanced /path/to/zim/files
python -m openzim_mcp --mode advanced /path/to/zim/files

# For development (from source)
uv run python -m openzim_mcp /path/to/zim/files
uv run python -m openzim_mcp --mode advanced /path/to/zim/files

# Or using make (development)
make run ZIM_DIR=/path/to/zim/files

Tool Modes

OpenZIM MCP supports two modes:

  • Simple Mode (default): Provides 1 intelligent tool (zim_query) that accepts natural language queries

  • Advanced Mode: Exposes all 21 specialized MCP tools for maximum control

MCP Configuration

Add the appropriate snippet to your MCP client's config file (claude_desktop_config.json, Cursor's MCP settings, etc.). The mcpServers wrapper is required by Claude Desktop, Cursor, and most other MCP clients.

Simple Mode (default):

{
  "mcpServers": {
    "openzim-mcp": {
      "command": "openzim-mcp",
      "args": ["/path/to/zim/files"]
    }
  }
}

Advanced Mode:

{
  "mcpServers": {
    "openzim-mcp-advanced": {
      "command": "openzim-mcp",
      "args": ["--mode", "advanced", "/path/to/zim/files"]
    }
  }
}

Alternative configuration using Python module:

{
  "mcpServers": {
    "openzim-mcp": {
      "command": "python",
      "args": [
        "-m",
        "openzim_mcp",
        "/path/to/zim/files"
      ]
    }
  }
}

For development (from source):

{
  "mcpServers": {
    "openzim-mcp": {
      "command": "uv",
      "args": [
        "--directory",
        "/path/to/openzim-mcp",
        "run",
        "python",
        "-m",
        "openzim_mcp",
        "/path/to/zim/files"
      ]
    }
  }
}

Development

Running Tests

# Run all tests
make test

# Run tests with coverage
make test-cov

# Run specific test file
uv run pytest tests/test_security.py -v

# Run tests with ZIM test data (comprehensive testing)
make test-with-zim-data

# Run integration tests only
make test-integration

# Run tests that require ZIM test data
make test-requires-zim-data

ZIM Test Data Integration

OpenZIM MCP integrates with the official zim-testing-suite for comprehensive testing with real ZIM files:

# Download essential test files (basic testing)
make download-test-data

# Download all test files (comprehensive testing)
make download-test-data-all

# List available test files
make list-test-data

# Clean downloaded test data
make clean-test-data

The test data includes:

  • Basic files: Small ZIM files for essential testing

  • Real content: Actual Wikipedia/Wikibooks content for integration testing

  • Invalid files: Malformed ZIM files for error handling testing

  • Special cases: Embedded content, split files, and edge cases

Test files are automatically organized by category and priority level.

Code Quality

# Format code
make format

# Run linting
make lint

# Type checking
make type-check

# Run all checks
make check

Project Structure

openzim-mcp/
├── openzim_mcp/                # Main package
│   ├── __init__.py             # Package init, exports __version__ via importlib.metadata
│   ├── __main__.py             # Module entry point (`python -m openzim_mcp`)
│   ├── main.py                 # CLI entry point and arg parsing
│   ├── server.py               # MCP server setup, transport selection
│   ├── http_app.py             # Streamable HTTP / SSE transport, auth, CORS, health
│   ├── config.py               # Pydantic config + env var bindings
│   ├── defaults.py             # Default values and tunables
│   ├── security.py             # Path validation, traversal protection, sanitization
│   ├── error_messages.py       # User-facing error message catalog
│   ├── exceptions.py           # Custom exception hierarchy
│   ├── cache.py                # LRU cache with TTL
│   ├── rate_limiter.py         # Per-client + global token-bucket rate limiting
│   ├── content_processor.py    # HTML→text, heading-id, link extraction
│   ├── async_operations.py     # asyncio helpers and timeouts
│   ├── timeout_utils.py        # Timeout primitives
│   ├── subscriptions.py        # MtimeWatcher and SubscriberRegistry
│   ├── simple_tools.py         # Simple-mode `zim_query` tool
│   ├── intent_parser.py        # Natural-language intent parsing
│   ├── types.py                # Shared TypedDicts
│   ├── constants.py            # Shared constants
│   ├── zim_operations.py       # Backward-compat shim re-exporting from zim/ package
│   ├── zim/                    # ZIM access (split from monolithic zim_operations.py)
│   │   ├── __init__.py         # ZimOperations facade composed of mixins
│   │   ├── archive.py          # Archive open/close, file listing, name resolution
│   │   ├── content.py          # Entry retrieval, summaries, batch get
│   │   ├── namespace.py        # Namespace listing, browse, walk
│   │   ├── search.py           # Full-text + suggestion search; cursor pagination
│   │   └── structure.py        # Article structure, links, related articles
│   └── tools/                  # MCP tool registrations
│       ├── __init__.py
│       ├── file_tools.py       # list_zim_files
│       ├── content_tools.py    # get_zim_entry, get_zim_entries
│       ├── search_tools.py     # search_zim_file, search_all, find_entry_by_title
│       ├── navigation_tools.py # browse_namespace, walk_namespace, search_with_filters, get_search_suggestions
│       ├── structure_tools.py  # get_article_structure, extract_article_links, get_entry_summary, get_table_of_contents, get_binary_entry
│       ├── metadata_tools.py   # get_zim_metadata, get_main_page, list_namespaces
│       ├── server_tools.py     # get_server_health, get_server_configuration
│       ├── resource_tools.py   # MCP resources (zim://files, zim://{name}/...)
│       └── prompts.py          # MCP prompts (/research, /summarize, /explore)
├── tests/                      # Test suite (pytest)
├── website/                    # GitHub Pages site source
├── pyproject.toml              # Project configuration
├── Makefile                    # Development commands
├── Dockerfile                  # Multi-stage container build
└── README.md                   # This file

API Reference

Available Tools

list_zim_files - List all ZIM files in allowed directories

Optional parameters:

  • name_filter (string, default: ""): Case-insensitive substring; only files whose filename contains it are returned. Empty string lists everything. Useful for narrowing large listings (e.g. "wikipedia", "nginx").

search_zim_file - Search within ZIM file content

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • query (string): Search query term — required unless cursor is provided.

Optional parameters:

  • limit (integer, default: 10): Maximum number of results to return

  • offset (integer, default: 0): Starting offset for results (for pagination)

  • cursor (string): Opaque pagination token from a previous result's next_cursor. When provided, overrides offset/limit with the values encoded in the token, and supplies query if it was not given explicitly. Cursors are only valid for the query they were issued for.

get_zim_entry - Get detailed content of a specific entry in a ZIM file

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Entry path, e.g., 'A/Some_Article'

Optional parameters:

  • max_content_length (integer, default: 100000, minimum: 1000): Maximum length of returned content

Smart Retrieval Features:

  • Automatic Fallback: If direct path access fails, automatically searches for the entry and uses the exact path found

  • Path Mapping Cache: Caches successful path mappings for improved performance on repeated access

  • Enhanced Error Guidance: Provides clear guidance when entries cannot be found, suggesting alternative approaches

  • Transparent Operation: Works seamlessly regardless of path encoding differences (spaces vs underscores, URL encoding, etc.)

get_zim_entries - Batch retrieve multiple ZIM entries in one call

Pairs naturally with HTTP transport, where round-trip cost matters. Up to 50 entries per batch. Each entry resolves independently — per-entry failures do not abort the batch.

Required parameters:

  • entries (list): Either a list of entry-path strings (paired with zim_file_path default) OR a list of {zim_file_path, entry_path} dicts (for multi-archive batches). Limit: 50 per batch.

Optional parameters:

  • zim_file_path (string): Default archive path; required if entries are bare strings, optional when each dict carries its own.

  • max_content_length (integer): Per-entry max content length.

Returns: JSON {"results": [...], "succeeded": N, "failed": N}. Each result includes index (input order), success, and either content or error.

Notes: Rate limit is charged per entry, not per batch (anti-bypass).

get_zim_metadata - Get ZIM file metadata from M namespace entries

Required parameters:

  • zim_file_path (string): Path to the ZIM file

Returns: JSON string containing ZIM metadata including entry counts, archive information, and metadata entries like title, description, language, creator, etc.

get_main_page - Get the main page entry from W namespace

Required parameters:

  • zim_file_path (string): Path to the ZIM file

Returns: Main page content or information about the main page entry.

list_namespaces - List available namespaces and their entry counts

Required parameters:

  • zim_file_path (string): Path to the ZIM file

Returns: JSON string containing namespace information with entry counts, descriptions, and sample entries for each namespace (C, M, W, X, etc.).

browse_namespace - Browse entries in a specific namespace with pagination

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • namespace (string): Namespace to browse (C, M, W, X, A, I, etc.)

Optional parameters:

  • limit (integer, default: 50, range: 1-200): Maximum number of entries to return

  • offset (integer, default: 0): Starting offset for pagination

Returns: JSON string containing namespace entries with titles, content previews, and pagination information.

walk_namespace - Deterministic cursor-paginated namespace iteration

Unlike browse_namespace (which samples and may cap at 200 entries on large archives), walk_namespace scans the archive by entry ID from cursor onward. Pair the returned next_cursor with a follow-up call to walk the rest. done: true indicates iteration is complete. Use this for exhaustive enumeration — e.g. dumping every M/* metadata entry, or finding an entry whose path doesn't follow common patterns.

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • namespace (string): Namespace to walk (C, M, W, X, A, I, etc.)

Optional parameters:

  • cursor (integer, default: 0): Entry ID to resume from

  • limit (integer, default: 200, range: 1–500): Max entries per page

Returns: JSON with entries, next_cursor, and done flag.

search_with_filters - Search within ZIM file content with advanced filters

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • query (string): Search query term

Optional parameters:

  • namespace (string): Optional namespace filter (C, M, W, X, etc.)

  • content_type (string): Optional content type filter (text/html, text/plain, etc.)

  • limit (integer, default: 10, range: 1-100): Maximum number of results to return

  • offset (integer, default: 0): Starting offset for pagination

Returns: Filtered search results with namespace and content type information.

search_all - Search across every ZIM file in the allowed directories

Returns merged per-file results so the caller doesn't need to know which file holds the information. Files that can't be searched (corrupt, no full-text index) are skipped without aborting the rest.

Required parameters:

  • query (string): Search query term

Optional parameters:

  • limit_per_file (integer, default: 5, range: 1–50): Max hits per ZIM file

  • limit (integer): Alias for limit_per_file. If both are provided, limit_per_file wins.

Returns: JSON containing per-file result groups and counts of files searched, files-with-results, and files that failed.

find_entry_by_title - Resolve a title to one or more entry paths

Cheaper than full-text search when the caller knows the article title. Tries an exact normalized C/<Title> match first (fast path), then falls back to libzim's title-indexed suggestion search.

Required parameters:

  • zim_file_path (string): Path to the ZIM file (used unless cross_file=true)

  • title (string): Title or partial title to resolve (case-insensitive)

Optional parameters:

  • cross_file (boolean, default: false): If true, search across all allowed ZIM files

  • limit (integer, default: 10, range: 1–50): Max results to return

Returns: JSON with query, ranked results, fast_path_hit flag, and files_searched count.

get_search_suggestions - Get search suggestions and auto-complete

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • partial_query (string): Partial search query (minimum 2 characters)

Optional parameters:

  • limit (integer, default: 10, range: 1-50): Maximum number of suggestions to return

Returns: JSON string containing search suggestions based on article titles and content.

get_article_structure - Extract article structure and metadata

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Entry path, e.g., 'C/Some_Article'

Returns: JSON string containing article structure including headings, sections, metadata, and word count.

extract_article_links - Extract internal and external links from an article

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Entry path, e.g., 'C/Some_Article'

Returns: JSON string containing categorized links (internal, external, media) with titles and metadata.

get_related_articles - Find articles related to a given entry via outbound links

Composes extract_article_links and deduplicates internal links, returning up to limit outbound targets. (Inbound discovery was removed — it required a bounded full-archive scan that was too expensive for interactive use; reach for full-text search instead.)

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Source entry, e.g. 'C/Some_Article'

Optional parameters:

  • limit (integer, default: 10, range: 1–100): Max results

Returns: JSON with outbound_results.

get_entry_summary - Get a concise article summary

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Entry path, e.g., 'C/Some_Article'

Optional parameters:

  • max_words (integer, default: 200, range: 10-1000): Maximum number of words in the summary

Returns: JSON string containing a concise summary extracted from the article's opening paragraphs, with metadata including title, word count, and truncation status.

Features:

  • Extracts opening paragraphs while removing infoboxes, navigation, and sidebars

  • Provides quick article overview without loading full content

  • Useful for LLMs to understand article context before deciding to read more

get_table_of_contents - Extract hierarchical table of contents

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Entry path, e.g., 'C/Some_Article'

Returns: JSON string containing a hierarchical tree structure of article headings (h1-h6), suitable for navigation and content overview.

Features:

  • Hierarchical tree structure with nested children

  • Includes heading levels, text, and anchor IDs

  • Provides heading count and maximum depth statistics

  • Enables LLMs to navigate directly to specific sections

get_binary_entry - Retrieve binary content from a ZIM entry

Required parameters:

  • zim_file_path (string): Path to the ZIM file

  • entry_path (string): Entry path, e.g., 'I/image.png' or 'I/document.pdf'

Optional parameters:

  • max_size_bytes (integer): Maximum size of content to return (default: 10MB). Content larger than this will return metadata only.

  • include_data (boolean): If true (default), include base64-encoded data. Set to false to retrieve metadata only.

Returns:

JSON string containing:

  • path: Entry path in ZIM file

  • title: Entry title

  • mime_type: Content type (e.g., "application/pdf", "image/png")

  • size: Size in bytes

  • size_human: Human-readable size (e.g., "1.5 MB")

  • encoding: "base64" when data is included, null otherwise

  • data: Base64-encoded content (if include_data=true and under size limit)

  • truncated: Boolean indicating if content exceeded size limit

Use Cases:

  • Retrieve PDFs for processing with PDF parsing tools

  • Extract images for vision models or OCR tools

  • Get video/audio files for transcription services

  • Enable multi-agent workflows with specialized content processors


Examples

Listing ZIM files

{
  "name": "list_zim_files"
}

Response:

Found 1 ZIM files in 1 directories:

[
  {
    "name": "wikipedia_en_100_2025-08.zim",
    "path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "directory": "C:\\zim",
    "size": "310.77 MB",
    "modified": "2025-09-11T10:20:50.148427"
  }
]

Searching ZIM files

{
  "name": "search_zim_file",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "query": "biology",
    "limit": 3
  }
}

Response:

Found 51 matches for "biology", showing 1-3:

## 1. Taxonomy (biology)
Path: Taxonomy_(biology)
Snippet: #  Taxonomy (biology) Part of a series on
---
Evolutionary biology
Darwin's finches by John Gould

  * Index
  * Introduction
  * [Main](Evolution "Evolution")
  * Outline

## 2. Protein
Path: Protein
Snippet: #  Protein A representation of the 3D structure of the protein myoglobin showing turquoise α-helices. This protein was the first to have its structure solved by X-ray crystallography. Toward the right-center among the coils, a prosthetic group called a heme group (shown in gray) with a bound oxygen molecule (red).

## 3. Ant
Path: Ant
Snippet: #  Ant Ants
Temporal range: Late Aptian – Present
---
Fire ants
[Scientific classification](Taxonomy_\(biology\) "Taxonomy \(biology\)")
Kingdom:  | [Animalia](Animal "Animal")
Phylum:  | [Arthropoda](Arthropod "Arthropod")
Class:  | [Insecta](Insect "Insect")
Order:  | Hymenoptera
Infraorder:  | Aculeata
Superfamily:  |
Latreille, 1809[1]
Family:  |
Latreille, 1809

Getting ZIM entries

{
  "name": "get_zim_entry",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "entry_path": "Protein"
  }
}

Response:

# Protein

Path: Protein
Type: text/html
## Content

#  Protein

A representation of the 3D structure of the protein myoglobin showing turquoise α-helices. This protein was the first to have its structure solved by X-ray crystallography. Toward the right-center among the coils, a prosthetic group called a heme group (shown in gray) with a bound oxygen molecule (red).

**Proteins** are large biomolecules and macromolecules that comprise one or more long chains of amino acid residues. Proteins perform a vast array of functions within organisms, including catalysing metabolic reactions, DNA replication, responding to stimuli, providing structure to cells and organisms, and transporting molecules from one location to another. Proteins differ from one another primarily in their sequence of amino acids, which is dictated by the nucleotide sequence of their genes, and which usually results in protein folding into a specific 3D structure that determines its activity.

A linear chain of amino acid residues is called a polypeptide. A protein contains at least one long polypeptide. Short polypeptides, containing less than 20–30 residues, are rarely considered to be proteins and are commonly called peptides.

... [Content truncated, total of 56,202 characters, only showing first 1,500 characters] ...

Smart Retrieval in Action

Example: Automatic path resolution

{
  "name": "get_zim_entry",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "entry_path": "C/Test Article"
  }
}

Response (showing smart retrieval working):

# Test Article

Requested Path: C/Test Article
Actual Path: C/Test_Article
Type: text/html

## Content

# Test Article

This article demonstrates the smart retrieval system automatically handling
path encoding differences. The system tried "C/Test Article" directly,
then automatically searched and found "C/Test_Article".

... [Content continues] ...

get_server_health - Get server health and statistics

No parameters required.

Returns:

  • Overall status (healthy / warning / error)

  • Cache performance metrics (hits, misses, hit rate, size)

  • Directory and ZIM-file accessibility checks

  • Recommendations and warnings

  • Sanitized configuration summary

Example Response:

{
  "timestamp": "2026-05-03T10:42:11.123456",
  "status": "healthy",
  "server_name": "openzim-mcp",
  "uptime_info": {
    "process_id": "[REDACTED]",
    "started_at": "2026-05-03T10:30:00"
  },
  "configuration": {
    "allowed_directories": 1,
    "cache_enabled": true,
    "config_hash": "abc12345..."
  },
  "cache_performance": {
    "enabled": true,
    "size": 4,
    "max_size": 100,
    "hit_rate": 0.62
  },
  "health_checks": {
    "directories_accessible": 1,
    "zim_files_found": 3,
    "permissions_ok": true
  },
  "recommendations": [],
  "warnings": []
}

get_server_configuration - Get detailed server configuration

No parameters required.

Returns: Comprehensive server configuration plus diagnostics. Sensitive fields (PIDs, raw filesystem paths) are redacted/sanitized — diagnostic output is intended to be safe to paste into bug reports.

Example Response:

{
  "configuration": {
    "server_name": "openzim-mcp",
    "allowed_directories": ["[REDACTED]/zim"],
    "allowed_directories_count": 1,
    "cache_enabled": true,
    "cache_max_size": 100,
    "cache_ttl_seconds": 3600,
    "content_max_length": 100000,
    "content_snippet_length": 1000,
    "search_default_limit": 10,
    "config_hash": "abc12345...",
    "server_pid": "[REDACTED]"
  },
  "diagnostics": {
    "validation_status": "ok",
    "warnings": [],
    "recommendations": []
  },
  "timestamp": "2026-05-03T10:42:11.123456"
}

Additional Search Examples

Computer-related search:

{
  "name": "search_zim_file",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "query": "computer",
    "limit": 2
  }
}

Response:

Found 39 matches for "computer", showing 1-2:

## 1. Video game
Path: Video_game
Snippet: #  Video game First-generation _Pong_ console at the Computerspielemuseum Berlin
---
Platforms

## 2. Protein
Path: Protein
Snippet: #  Protein A representation of the 3D structure of the protein myoglobin showing turquoise α-helices. This protein was the first to have its structure solved by X-ray crystallography. Toward the right-center among the coils, a prosthetic group called a heme group (shown in gray) with a bound oxygen molecule (red).

Getting detailed content:

{
  "name": "get_zim_entry",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "entry_path": "Evolution",
    "max_content_length": 1500
  }
}

Response:

# Evolution

Path: Evolution
Type: text/html
## Content

#  Evolution

Part of the Biology series on
---
****
Mechanisms and processes

  * Adaptation
  * Genetic drift
  * Gene flow
  * History of life
  * Maladaptation
  * Mutation
  * Natural selection
  * Neutral theory
  * Population genetics
  * Speciation

... [Content truncated, total of 110,237 characters, only showing first 1,500 characters] ...

Advanced Knowledge Retrieval Examples

Getting ZIM metadata:

{
  "name": "get_zim_metadata",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim"
  }
}

Response:

{
  "entry_count": 100000,
  "all_entry_count": 120000,
  "article_count": 80000,
  "media_count": 20000,
  "metadata_entries": {
    "Title": "Wikipedia (English)",
    "Description": "Wikipedia articles in English",
    "Language": "eng",
    "Creator": "Kiwix",
    "Date": "2025-08-15"
  }
}

Browsing a namespace:

{
  "name": "browse_namespace",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "namespace": "C",
    "limit": 5,
    "offset": 0
  }
}

Response:

{
  "namespace": "C",
  "total_in_namespace": 80000,
  "offset": 0,
  "limit": 5,
  "returned_count": 5,
  "has_more": true,
  "entries": [
    {
      "path": "C/Biology",
      "title": "Biology",
      "content_type": "text/html",
      "preview": "Biology is the scientific study of life..."
    }
  ]
}

Filtered search:

{
  "name": "search_with_filters",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "query": "evolution",
    "namespace": "C",
    "content_type": "text/html",
    "limit": 3
  }
}

Getting article structure:

{
  "name": "get_article_structure",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "entry_path": "C/Evolution"
  }
}

Response:

{
  "title": "Evolution",
  "path": "C/Evolution",
  "content_type": "text/html",
  "headings": [
    {"level": 1, "text": "Evolution", "id": "evolution"},
    {"level": 2, "text": "History", "id": "history"},
    {"level": 2, "text": "Mechanisms", "id": "mechanisms"}
  ],
  "sections": [
    {
      "title": "Evolution",
      "level": 1,
      "content_preview": "Evolution is the change in heritable traits...",
      "word_count": 150
    }
  ],
  "word_count": 5000
}

Getting article summary:

{
  "name": "get_entry_summary",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "entry_path": "C/Evolution",
    "max_words": 100
  }
}

Response:

{
  "title": "Evolution",
  "path": "C/Evolution",
  "content_type": "text/html",
  "summary": "Evolution is the change in heritable characteristics of biological populations over successive generations. These characteristics are the expressions of genes, which are passed from parent to offspring during reproduction...",
  "word_count": 100,
  "is_truncated": true
}

Getting table of contents:

{
  "name": "get_table_of_contents",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "entry_path": "C/Evolution"
  }
}

Response:

{
  "title": "Evolution",
  "path": "C/Evolution",
  "content_type": "text/html",
  "toc": [
    {
      "level": 1,
      "text": "Evolution",
      "id": "evolution",
      "children": [
        {
          "level": 2,
          "text": "History of evolutionary thought",
          "id": "history",
          "children": []
        },
        {
          "level": 2,
          "text": "Mechanisms",
          "id": "mechanisms",
          "children": []
        }
      ]
    }
  ],
  "heading_count": 15,
  "max_depth": 4
}

Getting search suggestions:

{
  "name": "get_search_suggestions",
  "arguments": {
    "zim_file_path": "C:\\zim\\wikipedia_en_100_2025-08.zim",
    "partial_query": "bio",
    "limit": 5
  }
}

Response:

{
  "partial_query": "bio",
  "suggestions": [
    {"text": "Biology", "path": "C/Biology", "type": "title_start_match"},
    {"text": "Biochemistry", "path": "C/Biochemistry", "type": "title_start_match"},
    {"text": "Biodiversity", "path": "C/Biodiversity", "type": "title_start_match"}
  ],
  "count": 3
}

Server Management and Diagnostics Examples

Getting server health:

{
  "name": "get_server_health"
}

Response:

{
  "status": "healthy",
  "server_name": "openzim-mcp",
  "uptime_info": {
    "process_id": "[REDACTED]",
    "started_at": "2026-05-03T10:30:00"
  },
  "cache_performance": {
    "enabled": true,
    "size": 15,
    "max_size": 100,
    "hit_rate": 0.85
  }
}

ZIM Entry Retrieval Best Practices

Smart Retrieval System

OpenZIM MCP implements an intelligent entry retrieval system that automatically handles path encoding inconsistencies common in ZIM files:

How It Works:

  1. Direct Access First: Attempts to retrieve the entry using the provided path exactly as given

  2. Automatic Fallback: If direct access fails, automatically searches for the entry using various search terms

  3. Path Mapping Cache: Caches successful path mappings to improve performance for repeated access

  4. Enhanced Error Guidance: Provides clear guidance when entries cannot be found

Benefits for LLM Users:

  • Transparent Operation: No need to understand ZIM path encoding complexities

  • Single Tool Call: Eliminates the need for manual search-first methodology

  • Reliable Results: Consistent success across different path formats (spaces vs underscores, URL encoding, etc.)

  • Performance Optimized: Cached mappings improve repeated access speed

Example Scenarios Handled Automatically:

  • C/Test Article → C/Test_Article (space to underscore conversion)

  • C/Café → C/Caf%C3%A9 (URL encoding differences)

  • A/Some-Page → A/Some_Page (hyphen to underscore conversion)

Usage Recommendations

For Direct Entry Access:

{
  "name": "get_zim_entry",
  "arguments": {
    "zim_file_path": "/path/to/file.zim",
    "entry_path": "C/Article_Name"
  }
}

When Entry Not Found: The system will automatically provide guidance:

Entry not found: 'A/Article_Name'.
The entry path may not exist in this ZIM file.
Try using search_zim_file() to find available entries,
or browse_namespace() to explore the file structure.

Important Notes and Limitations

Content Length Requirements

  • The max_content_length parameter for get_zim_entry must be at least 1000 characters

  • Content longer than the specified limit will be truncated with a note showing the total character count

Search Behavior

  • Search results may include articles that contain the search terms in various contexts

  • Results are ranked by relevance but may not always be directly related to the primary meaning of the search term

  • Search snippets provide a preview of the content but may not show the exact location where the search term appears

File Format Support

  • Currently supports ZIM files (Zeno IMproved format)

  • Tested with Wikipedia ZIM files (e.g., wikipedia_en_100_2025-08.zim)

  • File paths must be properly escaped in JSON (use \\ for Windows paths)


Configuration

OpenZIM MCP supports configuration through environment variables with the OPENZIM_MCP_ prefix:

# Cache configuration
export OPENZIM_MCP_CACHE__ENABLED=true
export OPENZIM_MCP_CACHE__MAX_SIZE=200
export OPENZIM_MCP_CACHE__TTL_SECONDS=7200

# Content configuration
export OPENZIM_MCP_CONTENT__MAX_CONTENT_LENGTH=200000
export OPENZIM_MCP_CONTENT__SNIPPET_LENGTH=2000
export OPENZIM_MCP_CONTENT__DEFAULT_SEARCH_LIMIT=20

# Logging configuration
export OPENZIM_MCP_LOGGING__LEVEL=DEBUG
export OPENZIM_MCP_LOGGING__FORMAT="%(asctime)s - %(name)s - %(levelname)s - %(message)s"

# Server configuration
export OPENZIM_MCP_SERVER_NAME=my_openzim_mcp_server

Configuration Options

Setting

Default

Description

OPENZIM_MCP_TOOL_MODE

simple

Tool surface: simple (one zim_query tool) or advanced (21 specialized tools). Controlled by --tool-mode on the CLI as well.

OPENZIM_MCP_TRANSPORT

stdio

Transport protocol: stdio, http, or sse.

OPENZIM_MCP_HOST

127.0.0.1

HTTP/SSE bind host. Non-loopback hosts require OPENZIM_MCP_AUTH_TOKEN.

OPENZIM_MCP_PORT

8000

HTTP/SSE bind port.

OPENZIM_MCP_AUTH_TOKEN

(unset)

Bearer token required when binding HTTP/SSE to a non-loopback interface.

OPENZIM_MCP_CORS_ORIGINS

(empty)

JSON array of allowed CORS origins for the HTTP transport. Wildcard * is rejected.

OPENZIM_MCP_ALLOWED_HOSTS

(empty)

JSON array of public-facing hostnames the HTTP transport accepts in the Host header (e.g. ["mcp.example.com"]). Loopback is always allowed; this extends it for reverse-proxy and Tailscale-serve deployments. Wildcard * is rejected.

OPENZIM_MCP_SUBSCRIPTIONS_ENABLED

true

Enable MCP resource subscriptions (HTTP transport only). When false, subscribe calls succeed but no updates fire.

OPENZIM_MCP_WATCH_INTERVAL_SECONDS

5

Polling interval (1–60s) for the subscription mtime watcher.

OPENZIM_MCP_CACHE__ENABLED

true

Enable/disable caching

OPENZIM_MCP_CACHE__MAX_SIZE

100

Maximum cache entries

OPENZIM_MCP_CACHE__TTL_SECONDS

3600

Cache TTL in seconds

OPENZIM_MCP_CONTENT__MAX_CONTENT_LENGTH

100000

Max content length

OPENZIM_MCP_CONTENT__SNIPPET_LENGTH

1000

Max snippet length

OPENZIM_MCP_CONTENT__DEFAULT_SEARCH_LIMIT

10

Default search result limit

OPENZIM_MCP_LOGGING__LEVEL

INFO

Logging level

OPENZIM_MCP_LOGGING__FORMAT

%(asctime)s - %(name)s - %(levelname)s - %(message)s

Log message format

OPENZIM_MCP_SERVER_NAME

openzim-mcp

Server instance name


Security Features

  • Path Traversal Protection: Secure path validation prevents access outside allowed directories

  • Input Sanitization: All user inputs are validated and sanitized

  • Resource Management: Proper cleanup of ZIM archive resources

  • Error Handling: Sanitized error messages prevent information disclosure

  • Type Safety: Full type annotations prevent type-related vulnerabilities


Performance Features

  • Intelligent Caching: LRU cache with TTL for frequently accessed content

  • Resource Pooling: Efficient ZIM archive management

  • Optimized Content Processing: Fast HTML to text conversion

  • Lazy Loading: Components initialized only when needed

  • Memory Management: Proper cleanup and resource management


Testing

The project includes comprehensive testing with 80%+ coverage using both mock data and real ZIM files:

Test Categories

  • Unit Tests: Individual component testing with mocks

  • Integration Tests: End-to-end functionality testing with real ZIM files

  • Security Tests: Path traversal and input validation testing

  • Performance Tests: Cache and resource management testing

  • Format Compatibility: Testing with various ZIM file formats and versions

  • Error Handling: Testing with invalid and malformed ZIM files

Test Infrastructure

OpenZIM MCP uses a hybrid testing approach:

  1. Mock-based tests: Fast unit tests using mocked libzim components

  2. Real ZIM file tests: Integration tests using official zim-testing-suite files

  3. Automatic test data management: Download and organize test files as needed

Test Data Sources

  • Built-in test data: Basic test files included in the repository

  • zim-testing-suite integration: Official test files from the OpenZIM project

  • Environment variable support: ZIM_TEST_DATA_DIR for custom test data locations

# Run tests with coverage report
make test-cov

# View coverage report
open htmlcov/index.html

# Run comprehensive tests with real ZIM files
make test-with-zim-data

Test Markers

Tests are organized with pytest markers:

  • @pytest.mark.requires_zim_data: Tests requiring ZIM test data files

  • @pytest.mark.integration: Integration tests

  • @pytest.mark.slow: Long-running tests


Monitoring

OpenZIM MCP provides built-in monitoring capabilities:

  • Health Checks: Server health and status monitoring

  • Cache Metrics: Cache hit rates and performance statistics

  • Structured Logging: JSON-formatted logs for easy parsing

  • Error Tracking: Comprehensive error logging and tracking


Versioning

This project uses Semantic Versioning with automated version management through release-please.

Automated Releases

Version bumps and releases are automated based on Conventional Commits:

  • feat: - New features (minor version bump)

  • fix: - Bug fixes (patch version bump)

  • feat!: or BREAKING CHANGE: - Breaking changes (major version bump)

  • perf: - Performance improvements (patch version bump)

  • docs:, style:, refactor:, test:, chore: - No version bump

Release Process

The project uses an improved, consolidated release system with automatic validation:

  1. Automatic (Recommended): Push conventional commits → Release Please creates PR → Merge PR → Automatic release

  2. Manual: Use GitHub Actions UI for direct control over releases

  3. Emergency: Push tags directly for critical fixes

Key Features:

  • Zero-touch releases from main branch

  • Automatic version synchronization validation

  • Comprehensive testing before every release

  • Improved error handling and rollback capabilities

  • Branch protection prevents broken releases

The release flow is implemented in .github/workflows/release-please.yml and .github/workflows/release.yml.

Commit Message Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Examples:

feat: add search suggestions endpoint
fix: resolve path traversal vulnerability
feat!: change API response format
docs: update installation instructions

Contributing

  1. Fork the repository

  2. Create a feature branch (git checkout -b feature/amazing-feature)

  3. Make your changes

  4. Run tests (make check)

  5. Use conventional commit messages (git commit -m 'feat: add amazing feature')

  6. Push to the branch (git push origin feature/amazing-feature)

  7. Open a Pull Request

Development Guidelines

  • Follow PEP 8 style guidelines

  • Add type hints to all functions

  • Write tests for new functionality

  • Update documentation as needed

  • Use conventional commit messages for automatic versioning

  • Ensure all tests pass before submitting


License

This project is licensed under the MIT License - see the LICENSE file for details.


Acknowledgments

  • Kiwix for the ZIM format and libzim library

  • MCP for the Model Context Protocol

  • The open-source community for the excellent libraries used in this project


Made with ❤️ by Cameron Rye

Available Tools

8 tools
zim_browseA
Read-only

Browse a ZIM archive's namespace — paginated lookup or full walk.

EXTRACT whether the caller wants a paginated page or a full walk before calling. Most read-the-table-of-contents-style requests are mode="page"; only full-enumeration tasks (e.g. "list every article in namespace A") need mode="walk".

ALIASES: "browse ", "list ", "walk namespace ". Route through THIS tool with the matching mode.

PARAMETERS: zim_file_path REQUIRED. The archive to browse. namespace REQUIRED. ZIM namespace letter (e.g. "C" for content, "A" for articles in legacy archives, "I" for images). mode "page" (default) — paginated browse. "walk" — full namespace enumeration. cursor Opaque pagination handle from next_cursor. limit Page size: page 1-200 (default 50), walk 1-500 (default 200). offset Page-mode pagination offset (walk rejects it). include_assets Default False hides assets (css/js/fonts/images/ media) in C-browse; True surfaces them, e.g. media paths for zim_get(binary=True).

RESPONSE: BrowseNamespaceResponse (mode="page") or WalkNamespaceResponse (mode="walk"). Both carry results, next_cursor, and page_info.

ERRORS: Invalid mode returns invalid_mode; an empty namespace returns a validation envelope. An unknown namespace letter is a soft reject (isError=false): _meta.reason: "bad_namespace", plus page-only total: 0/discovery_method: "rejected_unknown_namespace".

NameRequiredDescriptionDefault
modeNopage
limitNo
cursorNo
offsetNo
namespaceYes
zim_file_pathYes
include_assetsNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already mark this as read-only and non-open-world, so the safety profile is known. The description adds valuable behavioral detail: walk rejects offset, include_assets hides or surfaces asset entries, pagination uses an opaque cursor, and unknown namespace letters produce a soft reject with specific _meta.reason values. This goes well beyond what annotations provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose, then uses clear section headers for usage, aliases, parameters, response, and errors. Although long, every sentence earns its place: parameter semantics are essential given the bare schema, and the error/response notes prevent misinvocation. No fluff or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With seven parameters, two modes, no output schema, and complex error behavior, this description is complete enough for an agent to call the tool correctly. It covers parameter choice, pagination state, response type names, and soft-error semantics, leaving no obvious operational gap.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 0% description coverage, so the description carries the full burden — and it succeeds. Every parameter is explained: zim_file_path and namespace are marked required, namespace letters are exemplified, mode values and defaults are given, cursor is identified as an opaque handle, limit ranges differ by mode, and offset's incompatibility with walk is stated.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource — 'Browse a ZIM archive's namespace' — and immediately clarifies the two operational modes, 'paginated lookup' and 'full walk'. It names common aliases ('browse <namespace>', 'list <namespace>') and routes them to this tool, making it easy to distinguish from siblings like zim_query or zim_search.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly instructs the agent to decide between 'page' and 'walk' before calling, and gives concrete selection criteria: read-the-table-of-contents-style requests use 'page', full-enumeration tasks use 'walk'. It also maps aliases to the correct mode, providing clear when-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

zim_getA
Read-only

Fetch entries from a ZIM archive — single, batch, binary, or main page.

EXTRACT the right path-shape before calling — the parameters form four mutually-exclusive branches:

  • Single entry, body view (default): pass entry_path + optional view. Returns the article body for view="full", a short summary for view="summary", a TOC tree for view="toc", a flat section list for view="structure".

  • Single entry, binary: pass entry_path + binary=True. Returns raw bytes (image, video, PDF, etc.). view is locked to "full" in this branch.

  • Batch: pass entry_paths (list of strings). Returns each entry's clean body, first page only; non-full view / content_offset return invalid_path_combination.

  • Main page: pass main_page=True (no entry_path). Returns the archive's main page. view, entry_path, entry_paths, binary are all forbidden in this branch.

ALIASES: callers may say "get article", "fetch", "show me ", "summary of ", "structure of ", "main page". Route through THIS tool with the matching branch.

PARAMETERS: zim_file_path REQUIRED. The archive containing the entry. entry_path Single-entry path (string). Mutually exclusive with entry_paths and main_page. entry_paths Batch-mode path list. Mutually exclusive with entry_path, binary, main_page. view Body slice when not binary/main_page: "full" (default, full markdown body), "summary" (short snippet), "toc" (heading tree), "structure" (flat section list). binary Default False. Set True to fetch raw bytes (single entry only). main_page Default False. Set True for the archive's main page (zero-path fetch). max_content_length Char cap for view="full" (default 100,000); with binary=True caps fetched bytes (default 10MB, oversize returns metadata + truncated: true). content_offset Char offset into the body for view="full" (default 0). Used with the truncation footer's pass content_offset=N hint. Single-entry only. compact Default False. Set True for small-LLM compaction. (zim_query defaults it True.) compact_budget Inert here — never forwarded. Only zim_query honors it.

RESPONSE: Branch-dependent dict — EntryResponse / BatchEntryResponse / EntrySummaryResponse / TableOfContentsResponse / ArticleStructureResponse / BinaryEntryResponse — or ToolErrorPayload on invalid combinations (invalid_path_combination).

ERRORS: Invalid branch combinations return structured invalid_path_combination; message names the conflict. Defense-in-depth: even if a small model flattens the wire-schema oneOf and sends an impossible payload, the handler rejects it cleanly.

NameRequiredDescriptionDefault
viewNofull
binaryNo
compactNo
main_pageNo
entry_pathNo
entry_pathsNo
zim_file_pathYes
compact_budgetNo
content_offsetNo
max_content_lengthNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes well beyond the readOnlyHint annotation by disclosing truncation behavior, content_offset continuation hints, binary mode's view lock, and the fact that compact_budget is never forwarded. It also explains the structured invalid_path_combination error and defense-in-depth rejection behavior, all of which are valuable for an agent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is long, but it is densely informative and well-structured with labeled sections: branch summary, aliases, parameters, response, and errors. Every sentence adds necessary call guidance, and the front-loaded overview helps an agent quickly understand the tool's shape.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the absence of an output schema, the RESPONSE section properly enumerates the branch-dependent response types and the error payload. The ERROR section clarifies how invalid combinations are reported, so an agent has sufficient context to invoke the tool and interpret results correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 0% description coverage, so the PARAMETERS section carries the full burden. It explains every parameter's role, default, mutual exclusions, and special interactions, such as max_content_length capping bytes when binary=True and view being locked to 'full' in binary mode. This fully compensates for the schema's lack of descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific action and object: 'Fetch entries from a ZIM archive — single, batch, binary, or main page.' It then enumerates the four distinct retrieval modes, making it clear what this tool does and how it differs from searching or querying. The alias list further reinforces the intended purpose.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly instructs the caller to select one of four mutually exclusive branches and details which parameters to pass for each. It also names forbidden combinations, such as non-full views in batch mode and extra parameters in main-page mode. The note that compact_budget is only honored by zim_query provides a clear cross-tool alternative.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

zim_get_sectionA
Read-only

Fetch one named section of an article — by section_id (from the TOC), with optional subsection inclusion.

EXTRACT the section id before calling — zim_get(view="toc") lists them if the caller didn't supply one.

ALIASES: callers may say "section of ", "show me the section", " section ". Route through THIS tool.

PARAMETERS: zim_file_path REQUIRED. The archive containing the article. entry_path REQUIRED. The article whose section to fetch. section_id REQUIRED. The TOC id (e.g. "History"). max_chars Optional char cap on the section body. include_subsections Default True: include nested subsections. False: stop at the next heading of any level. compact Default True: oversized tables become placeholders and link markup is stripped (the zim_get compact=True shape). False: raw body with full tables and links. compact_budget Inert — never forwarded. Use max_chars to cap this tool; only zim_query honors this one.

RESPONSE: GetSectionResponse — section body markdown, metadata, and any nested subsections.

ERRORS: Unknown section_id → ToolErrorPayload with available_section_ids (not a hint) and closest_match. Missing entry → entry_not_found.

NameRequiredDescriptionDefault
compactNo
max_charsNo
entry_pathYes
section_idYes
zim_file_pathYes
compact_budgetNo
include_subsectionsNo

TDQS

A5/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare readOnlyHint and openWorldHint; the description goes well beyond by documenting that compact strips link markup and replaces oversized tables, compact_budget is inert and never forwarded, include_subsections=False stops at the next heading, and error payloads expose available_section_ids and closest_match rather than a hint.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded purpose, then compact label-style sections for aliases, parameters, response, and errors. Every sentence adds semantic value; the parameter annotations are organized and directly scannable.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with no output schema and no schema-level parameter descriptions, the response shape and error cases are spelled out. The combination of prerequisites (get section_id from TOC), parameter behavior, response contents, and error payloads leaves no critical gap for correct invocation.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden, and it succeeds: all seven parameters are explained, with defaults for include_subsections and compact, and the inert nature of compact_budget is explicitly called out. Error semantics for section_id are also covered.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The first sentence identifies a specific verb and resource: 'Fetch one named section of an article' by section_id from the TOC, with optional subsection inclusion. This makes it clearly distinct from siblings like zim_get (whole article/TOC) and zim_query.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It explicitly tells the agent to extract section_id before calling and points to zim_get(view='toc') as the source when the caller didn't supply one. The ALIASES block gives concrete caller phrasings and directs routing through this tool, and it warns that compact_budget is only honored by zim_query, so the agent knows not to use it here.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

zim_healthA
Read-only

Inspect the openzim-mcp server's state — or validate one archive — in one call.

With NO argument: returns health checks (cache stats, directory probes, recommendations), configuration (allowed directories, cache config, server PID), and loaded_archives (every *.zim found; readable: false marks files that fail the ZIM signature check). Collapses the legacy get_server_health + get_server_configuration + list_zim_files triple into one answer to "what is this server, what does it have, and is it OK".

With a zim_file_path: validates/diagnoses that one archive — runs Archive.check() (integrity), reports checksum, index, and identity.

ALIASES: "is the server ok", "list archives", "what's loaded", "server health" (no arg); "validate this zim", "is this archive corrupt" (with path).

PARAMETERS: zim_file_path OPTIONAL. Omit for server state; pass to validate one archive.

RESPONSE: No arg → ServerHealthResponse {health, configuration, loaded_archives, _meta}. With path → ArchiveValidationResponse {is_valid (check() result), has_checksum, checksum, has_fulltext_index, has_title_index, uuid, is_multipart, path, name, _meta}.

NameRequiredDescriptionDefault
zim_file_pathNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, but the description adds substantial behavioral detail beyond that: it describes health checks, configuration fields, loaded_archives, the meaning of readable:false, and the validation behavior including Archive.check() and the returned fields. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is longer than average but well-structured with clear sections and front-loaded information. Most sentences add value, especially the response field enumeration, which is necessary because no output schema exists. Minor redundancy exists around the omit/pass guidance, but it is not wasteful.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given there is no output schema, the description thoughtfully includes the full response shapes for both invocation modes. The single optional parameter is fully explained, and the tool's behavior is covered for both branches. Nothing an agent needs to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema provides no property descriptions, so schema coverage is 0%. The description fully compensates by explaining that zim_file_path is optional, what omitting it does, and what passing it does. This is exactly the semantic guidance the schema lacks.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb and resource: 'Inspect the openzim-mcp server's state — or validate one archive — in one call.' It clearly distinguishes the tool from the content-access siblings by positioning it as a health/state/validation tool rather than a content-retrieval tool.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives explicit usage conditions: no argument for server state, a zim_file_path for validating one archive. It also supplies helpful aliases. It does not explicitly contrast with sibling tools like zim_search or zim_get, but the behavioral split is clear enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

zim_metadataA
Read-only

Inspect a ZIM archive's metadata + namespace inventory.

Returns the M-namespace fields (Name, Title, Creator, Date, …) plus the per-namespace entry counts in one combined response. Replaces the legacy get_zim_metadata + list_namespaces pair.

ALIASES: callers may say "metadata for ", "what's in this zim", "describe the archive". Route through THIS tool.

PARAMETERS: zim_file_path REQUIRED. The archive to inspect.

RESPONSE: ArchiveMetadataResponse with: - metadata: flat dict[str, str] of M-namespace fields. - namespaces: list of NamespaceInfo (letter + total + diagnostics). - archive_identity {uuid, is_multipart} + index_capabilities {has_fulltext_index, has_title_index} — identity and whether search / suggestions will work. - counter_breakdown {mimetype: count} parsed from M/Counter; omitted when absent. - _meta: standard envelope.

NO main_page_path field. The canonical main-page fetch is zim_get(main_page=True) — surfacing the path here would create two routes a small model would null-check unnecessarily.

ERRORS: Missing/invalid zim_file_path returns a structured error envelope.

NameRequiredDescriptionDefault
zim_file_pathYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint=true annotation, the description discloses the full response shape (metadata dict, namespaces list, archive_identity, index_capabilities), the conditional omission of counter_breakdown, the deliberate exclusion of main_page_path with rationale, and a structured error envelope for invalid input. No contradiction with the read-only / non-open-world annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The purpose is front-loaded and the body is cleanly sectioned (PARAMETERS, RESPONSE, ERRORS, ALIASES) with bolded callouts for the main_page_path exclusion. The length is justified because the RESPONSE section carries documentation that would otherwise be missing given there is no output schema; every sentence adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter, read-only tool with no output schema, the description is remarkably complete: it documents response fields, conditional fields, exclusions and their rationale, error behavior, aliases, and the legacy tools it replaces. Nothing an agent needs to invoke it correctly appears to be missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate; it states the parameter is REQUIRED and identifies 'the archive to inspect', which adds only modest meaning over the self-descriptive name zim_file_path. It doesn't specify the expected form (local path vs identifier) or resolution behavior, though the error note softens this gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description opens with a specific verb-resource pair ('Inspect a ZIM archive's metadata + namespace inventory') and details the exact payload (M-namespace fields plus per-namespace counts). It distinguishes itself from siblings by explicitly excluding main_page_path and routing that need to zim_get, and it lists natural-language aliases to aid intent matching.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides explicit routing guidance: the ALIASES section tells callers to 'Route through THIS tool' for phrases like 'what's in this zim', and the main_page_path note gives a concrete when-not with an alternative (zim_get(main_page=True)). It also documents that it replaces the legacy get_zim_metadata + list_namespaces pair.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

zim_queryA
Read-only

Query ZIM archives using natural language.

Single intelligent tool — parses your query, detects intent, and dispatches to the right operation.

EXTRACT INTENT BEFORE CALLING. Do not pass the user's raw message as query. Translate it into one of the operations below: "test this tool" -> query="list available ZIM files" "what's in here" -> query="show main page" "explore" -> query="list namespaces" "tell me about cats" -> query="tell me about cats" -> query=""

ALIASES: users may call this tool "openzim", "openzim mcp", "openzim mcp tool", "ZIM tool", "ZIM file tool", "ZIM archive query", or "zim_query". All mean THIS tool — always call it; never claim it does not exist.

OPERATIONS (pass one as query): list available ZIM files - list loaded archives show main page - active archive main page list namespaces - list entry types metadata for - archive metadata tell me about - fetch article (auto on strong title match) search for - full-text search get article - fetch specific article show structure of - section outline links in - article-out links suggestions for - title autocomplete browse namespace - list namespace entries search in namespace - filtered search search all files for - cross-archive search walk namespace - enumerate namespace find article titled - title lookup articles related to - related articles what links to - article-in links summary of - lead summary table of contents - heading list section of - one section's body get image - binary bytes (base64) get articles , - batch fetch

Args: query: REQUIRED. Translated from user intent — never the user's raw message. zim_file_path: Optional. Omit entirely (recommended) — the tool auto-selects the loaded archive (or opens all of them when synthesize=True). Pass a real path ONLY when multiple archives are loaded and you need to target a specific one; call list available ZIM files first to see the real paths. NEVER pass an article title, topic, or made-up filename here, and do NOT invent a path from this docstring — paths that don't match a loaded archive are silently auto-corrected when only one archive is loaded, and surface a path-listing error otherwise. limit: Max results to return. When omitted, each list intent applies its own per-intent default (search 10, browse 50, walk 200, links 25, search-all 5/archive); pass a value to override. Ignored for atomic intents that return a single item or a fixed-shape payload — get article <name>, show structure of <name>, show main page, list namespaces, metadata for <file>, list available ZIM files, summary of <name>, table of contents <name>, section <X> of <name>. Setting it there has no effect; omit it on those calls. offset: Pagination offset (default: 0). Not honoured by suggestions for <prefix>, find article titled <name> or articles related to <name>; raise limit there instead. cursor: Opaque handle from a prior next_cursor. max_content_length: Article body cap (default: 4000). content_offset: Character offset to start reading the article body from (default: 0). The truncation footer on long articles surfaces a pass content_offset=N hint — wire that value back here to read the next page. Negative values are rejected with an invalid_content_offset error. compact: When True (the default in both modes), apply small-LLM optimizations — strip markdown link-soup, drop section previews from structure responses, flatten link/title/related listings into compact markdown, fetch only the article lead section, and cap total response size. Set False for the verbose advanced-mode-style response. compact_budget: Hard char-cap on the final response when compact=True. Accepts either a named profile — "tiny" (2 000), "small" (4 000), "medium" (6 000, default), "large" (12 000) — or a raw integer. Size it to the calling model's context window. Has no effect when compact=False. synthesize: When True, bypass intent classification and run the synthesize pipeline — multi-archive Xapian search, RRF fusion, passage extraction, section attribution, and citation rendering. Returns a SynthesizeResponse dict instead of markdown text. Defaults to False (legacy markdown path unchanged). NOTE: this is a mode toggle, not a "search harder" flag. Don't flip it on a follow-up just because the previous response was unhelpful — refine the query or offset instead. The synthesize pipeline runs one structured query and returns one answer; calling it twice with the same query yields the same answer.

Returns: Markdown string (synthesize=False) or SynthesizeResponse dict (synthesize=True) with answer_markdown, passages, citations, and archives_searched.

NameRequiredDescriptionDefault
limitNo
queryYes
cursorNo
offsetNo
compactNo
synthesizeNo
zim_file_pathNo
compact_budgetNo
content_offsetNo
max_content_lengthNo

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description goes far beyond the readOnlyHint annotation by disclosing silent path auto-correction, per-intent limit defaults, parameters that are ignored on certain operations, the truncation/content_offset loop, compact mode behavior, and synthesize mode semantics. There is no contradiction with the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The definition is long, but the tool is genuinely complex and the structure—headers, operation list, per-parameter notes—makes it navigable. Minor redundancy between the EXTRACT INTENT examples and the operations list keeps it from a perfect conciseness score.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a 10-parameter tool with no output schema and no inline parameter descriptions, the description covers the full calling contract: input transformation, return format, pagination semantics, per-intent defaults, and mode toggles. Nothing essential for correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must carry the full semantic burden, and it does: every parameter gets operational guidance, examples, defaults, ignored-case caveats, or failure modes. This is exemplary compensation for a bare schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description names a specific verb and resource ('Query ZIM archives using natural language') and immediately defines itself as the single natural-language dispatcher, which sets it apart from the focused sibling tools even though their names are not repeated in the text. The operation list makes the scope concrete and unmistakable.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives strong usage guidance: translate intent before calling, never pass raw user messages, treat aliases as triggers for this tool, and omit zim_file_path unless targeting a specific loaded archive. It does not, however, explicitly explain when to prefer this dispatcher over specialized siblings like zim_search or zim_get.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 7 tool updatesv3.2.5
    • Addedzim_browse
    • Addedzim_get
    • Addedzim_get_section
    • Addedzim_health
    • Addedzim_links
    • Addedzim_metadata
    • Addedzim_search
  2. 1 tool updatev3.2.1
    • Changedzim_query16 fields changed
      • removedInput schema / properties / compact / title
        Removed value: -"Compact"
      • removedInput schema / properties / compact_budget / default
        Removed value: -null
      • removedInput schema / properties / compact_budget / title
        Removed value: -"Compact Budget"
      • removedInput schema / properties / content_offset / title
        Removed value: -"Content Offset"
      • removedInput schema / properties / cursor / default
        Removed value: -null
      • removedInput schema / properties / cursor / title
        Removed value: -"Cursor"
      • removedInput schema / properties / limit / default
        Removed value: -null
      • removedInput schema / properties / limit / title
        Removed value: -"Limit"
      • removedInput schema / properties / max_content_length / default
        Removed value: -null
      • removedInput schema / properties / max_content_length / title
        Removed value: -"Max Content Length"
      • removedInput schema / properties / offset / title
        Removed value: -"Offset"
      • removedInput schema / properties / query / title
        Removed value: -"Query"
      • removedInput schema / properties / synthesize / title
        Removed value: -"Synthesize"
      • removedInput schema / properties / zim_file_path / default
        Removed value: -null
      • removedInput schema / properties / zim_file_path / title
        Removed value: -"Zim File Path"
      • removedInput schema / title
        Removed value: -"zim_queryArguments"
  3. 1 tool updatev2.6.0
    • Changedzim_query1 field changed
      • changedOutput schema / (root)
        Previous value: -{
        -  "$defs": {
        -    "Citation": {
        -      "description": "A citation in a SynthesizeResponse.\n\n``total=False`` lets D8 (v2.0.0a9) attach ``rank`` and ``score``\nin compact-mode synthesize responses — fields the verbose path\nkeeps on ``SynthesizePassage`` but compact mode drops the\npassages array entirely to save tokens. Compact callers correlate\nrank/score with the citation directly.",
        -      "properties": {
        -        "archive": {
        -          "title": "Archive",
        -          "type": "string"
        -        },
        -        "cite_id": {
        -          "title": "Cite Id",
        -          "type": "string"
        -        },
        -        "entry_path": {
        -          "title": "Entry Path",
        -          "type": "string"
        -        },
        -        "rank": {
        -          "title": "Rank",
        -          "type": "integer"
        -        },
        -        "score": {
        -          "title": "Score",
        -          "type": "number"
        -        },
        -        "section_id": {
        -          "anyOf": [
        -            {
        -              "type": "string"
        -            },
        -            {
        -              "type": "null"
        -            }
        -          ],
        -          "title": "Section Id"
        -        },
        -        "section_title": {
        -          "anyOf": [
        -            {
        -              "type": "string"
        -            },
        -            {
        -              "type": "null"
        -            }
        -          ],
        -          "title": "Section Title"
        -        },
        -        "title": {
        -          "title": "Title",
        -          "type": "string"
        -        }
        -      },
        -      "title": "Citation",
        -      "type": "object"
        -    },
        -    "ConsideredArticle": {
        -      "description": "A14: an article hit not selected as the featured citation, surfaced\nso the caller can pivot in a follow-up turn without re-running search.\n\n``archive`` + ``entry_path`` form the handle the caller passes to\n``get_zim_entries`` (or composes into a ``cite_id``). ``score`` is the\nunderlying ranking score at the point of selection — informational,\nnot part of the handle.",
        -      "properties": {
        -        "archive": {
        -          "title": "Archive",
        -          "type": "string"
        -        },
        -        "entry_path": {
        -          "title": "Entry Path",
        -          "type": "string"
        -        },
        -        "score": {
        -          "title": "Score",
        -          "type": "number"
        -        },
        -        "title": {
        -          "title": "Title",
        -          "type": "string"
        -        }
        -      },
        -      "title": "ConsideredArticle",
        -      "type": "object"
        -    },
        -    "ConsideredSection": {
        -      "description": "A14: a section of the featured article not selected as the featured\npassage. ``section_id`` is the handle the caller passes to\n``get_section`` (or composes into a ``cite_id`` suffix).",
        -      "properties": {
        -        "section_id": {
        -          "title": "Section Id",
        -          "type": "string"
        -        },
        -        "title": {
        -          "title": "Title",
        -          "type": "string"
        -        }
        -      },
        -      "title": "ConsideredSection",
        -      "type": "object"
        -    },
        -    "MetaEnvelope": {
        -      "properties": {
        -        "chars": {
        -          "title": "Chars",
        -          "type": "integer"
        -        },
        -        "detected_type": {
        -          "title": "Detected Type",
        -          "type": "string"
        -        },
        -        "detection_confidence": {
        -          "title": "Detection Confidence",
        -          "type": "string"
        -        },
        -        "more_at_offset": {
        -          "title": "More At Offset",
        -          "type": "integer"
        -        },
        -        "preset_applied": {
        -          "title": "Preset Applied",
        -          "type": "string"
        -        },
        -        "reason": {
        -          "title": "Reason",
        -          "type": "string"
        -        },
        -        "suggestions": {
        -          "items": {
        -            "additionalProperties": {
        -              "type": "string"
        -            },
        -            "type": "object"
        -          },
        -          "title": "Suggestions",
        -          "type": "array"
        -        },
        -        "tokens_est": {
        -          "title": "Tokens Est",
        -          "type": "integer"
        -        },
        -        "total_chars": {
        -          "title": "Total Chars",
        -          "type": "integer"
        -        },
        -        "truncated": {
        -          "title": "Truncated",
        -          "type": "boolean"
        -        }
        -      },
        -      "title": "MetaEnvelope",
        -      "type": "object"
        -    },
        -    "SynthesizePassage": {
        -      "properties": {
        -        "cite_id": {
        -          "title": "Cite Id",
        -          "type": "string"
        -        },
        -        "rank": {
        -          "title": "Rank",
        -          "type": "integer"
        -        },
        -        "score": {
        -          "title": "Score",
        -          "type": "number"
        -        },
        -        "text_markdown": {
        -          "title": "Text Markdown",
        -          "type": "string"
        -        }
        -      },
        -      "required": [
        -        "cite_id",
        -        "text_markdown",
        -        "rank",
        -        "score"
        -      ],
        -      "title": "SynthesizePassage",
        -      "type": "object"
        -    },
        -    "SynthesizeResponse": {
        -      "properties": {
        -        "_meta": {
        -          "$ref": "#/$defs/MetaEnvelope"
        -        },
        -        "answer_markdown": {
        -          "title": "Answer Markdown",
        -          "type": "string"
        -        },
        -        "archives_searched": {
        -          "items": {
        -            "type": "string"
        -          },
        -          "title": "Archives Searched",
        -          "type": "array"
        -        },
        -        "citations": {
        -          "items": {
        -            "$ref": "#/$defs/Citation"
        -          },
        -          "title": "Citations",
        -          "type": "array"
        -        },
        -        "considered_articles": {
        -          "items": {
        -            "$ref": "#/$defs/ConsideredArticle"
        -          },
        -          "title": "Considered Articles",
        -          "type": "array"
        -        },
        -        "considered_sections": {
        -          "items": {
        -            "$ref": "#/$defs/ConsideredSection"
        -          },
        -          "title": "Considered Sections",
        -          "type": "array"
        -        },
        -        "fallback_used": {
        -          "enum": [
        -            "xapian_score",
        -            "rrf_fusion",
        -            "reranker"
        -          ],
        -          "title": "Fallback Used",
        -          "type": "string"
        -        },
        -        "passages": {
        -          "items": {
        -            "$ref": "#/$defs/SynthesizePassage"
        -          },
        -          "title": "Passages",
        -          "type": "array"
        -        },
        -        "query": {
        -          "title": "Query",
        -          "type": "string"
        -        },
        -        "total_chars": {
        -          "title": "Total Chars",
        -          "type": "integer"
        -        },
        -        "total_words": {
        -          "title": "Total Words",
        -          "type": "integer"
        -        }
        -      },
        -      "title": "SynthesizeResponse",
        -      "type": "object"
        -    },
        -    "ToolErrorPayload": {
        -      "description": "Envelope for tool errors returned via structuredContent.\n\n``error`` is always ``True`` so a client can branch on a single key\nwithout inspecting the operation name. ``message`` carries the\nsame human-readable text the tool would have returned as a string\n(markdown is fine — it's a string field, not nested JSON).",
        -      "properties": {
        -        "context": {
        -          "title": "Context",
        -          "type": "string"
        -        },
        -        "error": {
        -          "title": "Error",
        -          "type": "boolean"
        -        },
        -        "message": {
        -          "title": "Message",
        -          "type": "string"
        -        },
        -        "operation": {
        -          "title": "Operation",
        -          "type": "string"
        -        }
        -      },
        -      "required": [
        -        "error",
        -        "operation",
        -        "message"
        -      ],
        -      "title": "ToolErrorPayload",
        -      "type": "object"
        -    }
        -  },
        -  "properties": {
        -    "result": {
        -      "anyOf": [
        -        {
        -          "type": "string"
        -        },
        -        {
        -          "$ref": "#/$defs/SynthesizeResponse"
        -        },
        -        {
        -          "$ref": "#/$defs/ToolErrorPayload"
        -        }
        -      ],
        -      "title": "Result"
        -    }
        -  },
        -  "required": [
        -    "result"
        -  ],
        -  "title": "zim_queryOutput",
        -  "type": "object"
        -}New value: +null
  4. 1 tool updatev2.5.4
    • Changedzim_query3 fields changed
      • addedOutput schema / $defs / MetaEnvelope / properties / detected_type
        Added value: +{
        +  "title": "Detected Type",
        +  "type": "string"
        +}
      • addedOutput schema / $defs / MetaEnvelope / properties / detection_confidence
        Added value: +{
        +  "title": "Detection Confidence",
        +  "type": "string"
        +}
      • addedOutput schema / $defs / MetaEnvelope / properties / preset_applied
        Added value: +{
        +  "title": "Preset Applied",
        +  "type": "string"
        +}

TDQS

A4.7/5.0

Scored across 8 tools

Disambiguation3/5

zim_query is a catch-all tool that overlaps with zim_search, zim_get, zim_browse, and zim_links, creating ambiguity about when to use the general tool versus the specific ones. While each specific tool has a clear purpose, the existence of a meta-tool that handles the same intents increases the risk of misselection.

Naming Consistency5/5

All tools follow a consistent 'zim_<verb_noun>' pattern (e.g., zim_search, zim_get, zim_browse, zim_metadata, zim_links, zim_health, zim_get_section). The naming is uniform and predictable, with no mixing of conventions.

Tool Count5/5

With 8 tools, the server is well-scoped for its purpose of ZIM archive access. Each tool has a distinct role, and the count is within the ideal 3-15 range, providing enough granularity without overwhelming the agent.

Completeness5/5

The tool set comprehensively covers read operations for ZIM archives: article retrieval (zim_get), section fetching (zim_get_section), search (zim_search), browsing/namespace enumeration (zim_browse), metadata inspection (zim_metadata), link analysis (zim_links), and server health (zim_health). No obvious gaps exist for the stated read-only domain.

Maintenance

ActivityActive
ResponsivenessWithin a week

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables interaction with .zim archives by providing tools for article search, content retrieval, and metadata discovery. It features a TF-IDF based RAG engine for semantic retrieval over extracted article chunks from compressed ZIM files.
    GPL 3.0
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables large language models to directly access and search content in ZIM files, allowing offline question answering and information retrieval from resources like Wikipedia.
    22
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Provides offline search and retrieval of Wikipedia articles using Kiwix .zim files, enabling LLMs to access full Wikipedia content without internet.
    3
    MIT