Troubleshooting

Common issues, error messages, and solutions for OpenZIM MCP.

Notation: examples on this page use Python pseudo-call syntax (zim_health(), zim_search(query="...", mode="...")). The MCP wire format is JSON-RPC {"name": "...", "arguments": {...}} — your MCP client handles the framing. Tool names match the 8-tool advanced surface (zim_query, zim_search, zim_get, zim_get_section, zim_browse, zim_metadata, zim_links, zim_health).

Error response shape#

Before diving into specific failure modes — v2 changed how errors come back from tools. Every tool catches exceptions and returns a structured ToolErrorPayload instead of raising — except zim_query, whose guidance failures (no archive specified, an empty query/topic/search term, a meta-only or chained query) return markdown guidance and still carry isError: false:

{
  "error": true,
  "operation": "invalid_path_combination",
  "message": "Provide one of `entry_path`, `entry_paths`, or `main_page=True`."
}

Check for it with isinstance(result, dict) and result.get("error") is True. Since v2.6 the server also sets the MCP isError flag on these — EnvelopeAwareMCPServer.call_tool recognises the envelope on the way out and returns a CallToolResult with isError=True — so a client that branches on isError works too. No tool advertises an outputSchema, so nothing arrives in structuredContent; the envelope is JSON in the response’s text content block. Common error operation values you’ll see in this guide:

  • invalid_path_combinationzim_get got an impossible branch: entry_path + entry_paths, entry_paths + a non-full view, entry_paths + content_offset, binary + entry_paths/main_page/a non-full view, binary without entry_path, main_page + a path or a non-full view, or no selector at all
  • invalid_modezim_browse got a mode outside {page, walk}. This is the only enum rejection that reaches you under its own name, because zim_browse.mode is deliberately typed str with the enum attached as schema metadata so the handler gets to answer.
  • invalid_argument — every other out-of-range enum value. zim_get.view, zim_search.mode, and zim_links.direction / kind are Literal-typed, so pydantic rejects the call before the tool body runs and you get operation="invalid_argument" with an invalid_arguments key naming the field. The names invalid_view, invalid_direction and invalid_kind exist in the source but are unreachable over MCP — do not branch on them.
  • unknown_argument — an argument name the tool does not declare (a typo, or a parameter from a different tool). The envelope carries unknown_arguments, accepted_arguments, and closest_matches when something is near enough to suggest, so it is usually self-correcting. Emitted for every tool since v3.1.0.
  • invalid_sectionzim_get_section got an empty/missing section_id
  • invalid_content_offsetzim_get / zim_query got a negative content_offset
  • invalid_limitzim_search / zim_query got a limit below 1, or above the cap (50 for mode="title" / mode="suggest" and for cross_file=True; 100 for fulltext with namespace/content_type filters; 1000 for plain single-archive fulltext)
  • invalid_offsetzim_search / zim_query got a negative offset (offset=0 is the valid default)
  • invalid_max_content_lengthzim_get / zim_query got a max_content_length below 1 (zim_search has no such parameter)
  • invalid_combinationzim_search got mutually exclusive arguments (any cursor, a non-zero offset outside single-archive fulltext, cross_file + zim_file_path, mode="suggest" + cross_file, or namespace/content_type filters + cross_file)
  • missing_archivezim_search could not resolve an archive for mode="suggest", mode="title", or single-archive fulltext
  • invalid_queryzim_query got a query over the 4096-character cap
  • rate_limited — the token bucket for the internal operation was exhausted
  • cursor_decode — the cursor is malformed, truncated, or carries an unsupported version; on zim_query it is also the catch-all for every cursor fault, including one over the 2048-character cap. Drop it and restart the sequence
  • cursor_mismatch — the cursor was issued by a different tool. Advanced surface only; cursors are not portable across operations
  • cursor_context_mismatch — right tool, different target: another namespace, another entry, another link kind, or a changed include_assets. A cursor cannot be retargeted — start over for the new target
  • cursor_unsupported — only zim_links(direction="related"), which returns one ranked set and does not paginate. Omit cursor
  • See Cursors for which tools issue them, and for the two places where the usual “drop the cursor and pass offset” recovery does not work
  • file_not_found / entry_not_found / section_not_found / invalid_max_chars — data-layer failures surfaced by zim_get_section. section_not_found is the one envelope that carries extra keys: available_section_ids, available_section_ids_truncated, available_section_ids_total, and closest_match when a near match exists.
  • inbound_sidecar_unavailablezim_links(direction="inbound") against an archive with no link-graph sidecar
  • the tool’s own wire name — zim_get, zim_search, zim_query, zim_browse, zim_links, zim_metadata, zim_health, zim_get_section — when an unexpected exception escapes the tool body (archive read error, smart-retrieval miss). There is no get_entry or search operation value: those are internal rate-limiter bucket names, never envelope operations.
  • get server health / get server configuration — the only two operation values that are not snake_case, and the only two that never appear at the top level of a response. The no-argument zim_health() builds .health, .configuration and .loaded_archives independently, and each of the first two catches its own exceptions — so a failure inside one replaces that block alone with an envelope while the rest of the response still comes back. The top-level dict stays an ordinary success payload, so isError is false: test .health and .configuration for an error key rather than assuming the whole call succeeded. This partial case is the only unflagged one — a failure in the loaded_archives scan, or anywhere in the zim_health(zim_file_path=...) validation branch, surfaces as a normal top-level operation="zim_health" envelope with isError: true.
  • zim_query’s synthesize=True path adds synthesize_pipeline_error, synthesize_not_applicable, no_archives_available, meta_only_guidance, chained_intent_rejected, topic_required, search_terms_required, multi_entity_chain_rejected, and invalid_path. The default path also produces zim_path_not_found (a zim_file_path matching no loaded archive) and zim_query (any other exception the catch-all absorbs), plus the cursor_decode envelope a malformed or mismatched cursor produces. Only the guidance conditions — no archive specified, empty query, meta-only query, chained query — come back as markdown with isError: false.

The envelope itself is defined in openzim_mcp/responses.py — the ToolErrorPayload TypedDict and the tool_error() builder every failure path must go through — and the isError mapping in openzim_mcp/mcp_envelope.py. The underlying exception hierarchy in openzim_mcp/exceptions.py is what the broad-except paths wrap; it shapes the message text but not the envelope’s keys — error_code is never emitted.

Quick diagnostics#

First diagnostic#

Which check you can run depends on the tool surface you are running, and the default install is the narrow one — simple mode registers zim_query and nothing else, so zim_health is not there to call.

Every install — confirm which binary is answering:

openzim-mcp --version

It prints openzim-mcp followed by the installed version. A version older than you expect is the single most common cause of “the docs describe a parameter my server rejects”. A command not found here, when your MCP client reports the server failing to start, means the client’s PATH does not include the install location.

Simple mode (the default) — ask for the file list:

"list available ZIM files"

This is the only intent that reports on the server rather than reading an archive, and it exercises directory resolution, permissions and archive readability in a single call. The response names each archive with its path, size, modification time and a readable flag; an archive missing from it is an allowed-directories or file-permission problem, not a server fault.

Do not ask “check the server health and configuration” on a default install. There is no health intent in simple mode, so the phrase is parsed as a topic and searched inside the archive — what comes back is No search results found for "check the server health and configuration", which reads like a broken server and is not one.

Advanced mode — call zim_health:

zim_health()

Available once the surface is advanced (--mode advanced, OPENZIM_MCP_TOOL_MODE=advanced, or the published Docker image, which bakes it in). Returns combined health, resolved configuration and loaded-archives data in one response, consolidating three separate v1 tools into a single call.

Over HTTP — probe the endpoints directly:

curl -fsS http://localhost:8000/healthz
# {"status":"ok"}
curl -fsS http://localhost:8000/readyz
# {"status":"ready"}

Both are auth-exempt, so they work with no token and regardless of tool mode.

Installation issues#

Python version problems#

Error: Python 3.12+ required

Symptoms:

  • Server fails to start
  • Import errors during installation

Solutions:

  1. Check Python version: python --version
  2. Install Python 3.12+: Visit python.org
  3. Use correct Python: python3.12 -m openzim_mcp

Package installation failures#

Error: uv not found or pip install failed

Solutions:

  1. Install uv: curl -LsSf https://astral.sh/uv/install.sh | sh
  2. Use pip instead: pip install -e .
  3. Check permissions: Ensure write access to installation directory

Virtual environment issues#

Error: ModuleNotFoundError: No module named 'openzim_mcp'

Solutions:

  1. Activate virtual environment:

    # Windows
    venv\Scripts\activate
    # macOS/Linux
    source venv/bin/activate
  2. Reinstall in virtual environment: pip install -e .

ZIM file issues#

No ZIM files found#

Error: No ZIM files found in allowed directories from the file listing, or the No ZIM files found in any directory warning inside zim_health

Symptoms:

  • Empty .loaded_archives from zim_health
  • Server starts but no content available

Solutions:

  1. Check directory path: Verify the path exists and is correct
  2. Check file extensions: Ensure files have .zim extension
  3. Check permissions: Ensure read access to directory and files
  4. Download ZIM files: Get files from Kiwix Library

Permission denied#

Error: Permission denied: '/path/to/zim/files'

Solutions:

  1. Fix directory permissions: chmod 755 /path/to/zim/files
  2. Fix file permissions: chmod 644 /path/to/zim/files/*.zim
  3. Run as correct user: Ensure user has read access
  4. Check SELinux/AppArmor: May block file access on some systems

Corrupted ZIM files#

Error: Failed to open ZIM archive: ...file.zim (a corrupt or truncated archive) or File is not a ZIM file: ...file.txt (wrong extension)

Symptoms:

  • Search operations fail
  • Content retrieval errors
  • Metadata extraction fails

Solutions:

  1. Verify file integrity: Re-download the ZIM file
  2. Check file size: Compare with expected size from download source
  3. Test with different file: Try a known-good ZIM file
  4. Check disk space: Ensure sufficient space for file operations

MCP client configuration#

Server not responding#

Error: Connection refused or Server timeout

Symptoms:

  • MCP client can’t connect
  • Commands hang or timeout
  • No response from server

Solutions:

  1. Check server process: Ensure server is running
  2. Verify configuration: Check MCP client config file
  3. Check paths: Ensure all paths in config are correct
  4. Restart client: Restart MCP client application
  5. Check logs: Look for error messages in server output

Configuration file issues#

Error: Invalid configuration or Command not found

Common issues:

// Wrong - missing directory parameter
{
  "command": "python",
  "args": ["-m", "openzim_mcp"]
}

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

Solutions:

  1. Validate JSON: Use a JSON validator to check syntax
  2. Check paths: Ensure all paths exist and are accessible
  3. Use absolute paths: Avoid relative paths in configuration
  4. Test command manually: Run the command in terminal first

Search and content issues#

zim_search returns 0 results#

Error: none — a miss comes back as a success payload with "results": [] and "total": 0 (isError: false); simple-mode zim_query renders the same miss as No search results found for "<query>".

Possible causes:

  1. Typos in search terms: Check spelling
  2. Content not in ZIM file: Verify content exists
  3. Wrong namespace: Try different namespaces
  4. Search index issues: ZIM file may lack search index

Solutions:

  1. Try broader terms: Use more general search terms
  2. Browse namespaces: Use zim_browse to explore content
  3. Check ZIM metadata: Use zim_metadata to understand content (returns metadata + namespace counts in one payload)
  4. Try different ZIM files: Test with known-good content

Entry not found#

Error: Entry not found: 'A/Article_Name'

Smart retrieval: OpenZIM MCP automatically tries to find entries with different encodings, but sometimes manual intervention is needed.

Solutions:

  1. Use title-mode search first: zim_search(query="Article Name", mode="title") is the cheapest title-to-path resolver
  2. Check exact path: Use zim_browse(namespace="C") to find correct path
  3. Try different encodings:
    • A/Article_Name vs A/Article%20Name
    • A/Article_Name vs A/Article-Name
  4. Check namespace: Ensure correct namespace (A, C, etc.)

Content truncation#

Issue: Content appears cut off

Cause: max_content_length parameter limiting content

Solutions:

  1. Increase limit: Use higher max_content_length value
  2. Get structure first: Use zim_get(view="structure") or zim_get(view="toc") to understand content cheaply
  3. Request specific sections: Use zim_get_section(section_id=...) to target specific parts of long articles

Performance issues#

Slow response times#

Symptoms:

  • Long delays for search results
  • Timeouts on large operations
  • High memory usage

Solutions:

  1. Check cache settings: Ensure caching is enabled
  2. Reduce result limits: Use smaller limit values
  3. Monitor server health: Check cache hit rates
  4. Optimize ZIM files: Use smaller or more focused ZIM files
  5. Increase system resources: More RAM helps with large ZIM files

Memory issues#

Error: Out of memory or system becomes unresponsive

Solutions:

  1. Reduce cache size: Lower OPENZIM_MCP_CACHE__MAX_SIZE
  2. Use smaller ZIM files: Start with smaller content sets
  3. Limit concurrent operations: Avoid multiple large operations
  4. Increase system RAM: 2GB+ recommended for large ZIM files

Cache problems#

Issue: Poor cache performance or cache misses

Diagnostics: cache stats live inside zim_health under .health.cache_performance, so this needs the advanced surface:

zim_health()

The cache_performance block always has 13 fields — enabled, size, max_size, ancillary_entries, total_entries, size_bytes, max_bytes, ttl_seconds, hits, misses, hit_rate, background_cleanup, persistence_enabled — plus persistence_path and persistence_file_exists when persistence is on.

Solutions:

  1. Increase cache size: Raise OPENZIM_MCP_CACHE__MAX_SIZE.
  2. Increase TTL: Raise OPENZIM_MCP_CACHE__TTL_SECONDS.
  3. Flush the cache: There are no cache_clear / cache_stats / warm_cache tools — restart the server to flush. If persistence_enabled is true that is not enough on its own: the shutdown writes every unexpired entry back out and the next start reloads them, so stop the server, delete the file named by .health.cache_performance.persistence_path, then start it again. (For pre-warming after restart, call zim_get(entry_path=...) on your high-value entries from your client.)
  4. Aim for >70% hit rate for read-heavy workloads.

Security issues#

Path traversal warnings#

Error: Path contains suspicious pattern: ...file.zim or Access denied - Path is outside allowed directories: ...file.zim — both arrive as the **Technical Details** line of a **Security Validation Error** envelope.

Cause: Attempting to access files outside allowed directories

Solutions:

  1. Check file paths: Ensure paths are within allowed directories
  2. Use relative paths: Avoid ../ in file paths
  3. Verify configuration: Check allowed directories setting

Permission errors#

Error: Access denied or Permission denied

Solutions:

  1. Check file ownership: Ensure correct user owns files
  2. Fix permissions: Use chmod to set appropriate permissions
  3. Check parent directories: Ensure all parent directories are accessible
  4. Avoid running as root: Use appropriate user account

HTTP transport issues#

OpenZimMcpConfigurationError: HTTP transport bound to <host> requires authentication#

The safe-default startup check refuses to bind a non-loopback host without an auth token. Either:

export OPENZIM_MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
# or bind loopback only:
openzim-mcp --transport http --host 127.0.0.1 ...

OpenZimMcpConfigurationError: SSE transport bound to <host> is not allowed#

The SSE transport has no auth middleware in this server, so it must bind 127.0.0.1. For exposed deployments use --transport http (streamable HTTP) with OPENZIM_MCP_AUTH_TOKEN set.

401 unauthorized on /mcp#

Bearer token missing or wrong. Note OPTIONS /mcp is not exempt from auth (closed preflight-bypass attack surface) — a bare OPTIONS /mcp probe without a token gets 401. Browser CORS preflights are unaffected: when OPENZIM_MCP_CORS_ORIGINS is configured, the CORS middleware sits outside auth and answers the preflight itself (browsers never attach Authorization to a preflight).

/healthz and /readyz are exempt from auth and useful for unauthenticated probing.

Browser CORS errors when resuming sessions#

Mcp-Session-Id is in the server’s allow_headers and expose_headers since v1.0, and stays there for clients on the initialize handshake. If you have a reverse proxy in front that strips this header, their sessions can’t resume. A 2026-07-28 client has no session to resume — it needs MCP-Protocol-Version, Mcp-Method, and (for tools/call) Mcp-Name to survive the proxy instead; all three are in allow_headers.

OPENZIM_MCP_CORS_ORIGINS rejected at startup#

Wildcard "*" is rejected (whitespace-padded " * " too). List origins explicitly:

export OPENZIM_MCP_CORS_ORIGINS='["https://app.example.com","https://other.example.com"]'

UserWarning: Host 'localhost' does not resolve to loopback#

Your /etc/hosts maps localhost away from 127.0.0.1. The server emits a warning and treats the host as public, which then triggers the safe-default refusal. Bind explicitly to 127.0.0.1 or fix /etc/hosts.

Health endpoint behavior#

  • /healthz (liveness) — returns 200 OK if the process is running and the event loop is responsive. No auth.
  • /readyz (readiness) — returns 200 OK if at least one allowed directory is readable, 503 with {"status":"not_ready", "reason":"no readable allowed directories"} otherwise. No auth.

Per-entry resource issues#

zim://wikipedia/entry/A/Climate_change not found#

Clients MUST URL-encode / as %2F in the path segment because the SDK’s URI template engine treats / as a segment separator. Correct form:

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

A literal slash will fail to route. Other RFC 3986 reserved characters in the path also need encoding.

Subscription issues#

Subscriptions never fire#

  • Check OPENZIM_MCP_SUBSCRIPTIONS_ENABLED — set to false disables the watcher and withholds the capability: subscriptions/listen fails with method-not-found, so an acknowledged listen actually proves the subscription gate is open.
  • Notifications are published only under the HTTP transport (--transport http). On stdio and SSE the capability is not advertised and a listen request fails with method-not-found.
  • Check what your client asked for. A .zim appearing or disappearing publishes notifications/resources/list_changed (opt in with resourcesListChanged: true); only an in-place replacement publishes notifications/resources/updated, and only for that archive’s own zim://{name} URI. Listing zim://files under resourceSubscriptions never fires.
  • Tune OPENZIM_MCP_WATCH_INTERVAL_SECONDS (default 5, range 1-60). Slower poll = longer detection latency.
  • Detection compares mtime and size — same-size archive replacement is caught via the mtime change, same-mtime rewrites via the size change.
  • A client that stops reading its stream overruns the SDK’s per-stream event backlog and has the stream ended; process notifications asynchronously and re-listen if the stream closes. There is no replay, so re-read the resource after re-listening.
  • resources/subscribe was removed by the 2026-07-28 revision and is not served on this build — it answers -32601 Method not found. A client that only speaks the initialize handshake cannot receive change notifications at all.

Configuration errors#

Cleaner OpenZimMcpConfigurationError instead of pydantic dump#

The server catches pydantic.ValidationError from OpenZimMcpConfig construction and re-surfaces it as OpenZimMcpConfigurationError with a human-readable message naming the offending field. If you’re grepping logs for “validation error”, search for OpenZimMcpConfigurationError instead.

Path traversal warnings show ...filename.zim#

Path-traversal rejections redact absolute filesystem paths to ...filename.zim form so the canonical allowed-directory layout is not leaked. The truncated form is expected, not corrupted output.

Advanced debugging#

Enable debug logging#

export OPENZIM_MCP_LOGGING__LEVEL=DEBUG
uv run python -m openzim_mcp /path/to/zim/files

Check server logs#

Look for error patterns in server output:

  • ERROR: Critical errors requiring attention
  • WARNING: Potential issues to investigate
  • INFO: Normal operation messages

Test with minimal setup#

  1. Use small ZIM file: Test with <100MB file
  2. Single operation: Test one tool at a time
  3. Fresh environment: Clean virtual environment
  4. Default configuration: No custom environment variables

Getting help#

Before asking for help#

  1. Check this guide: Review relevant sections above
  2. Run diagnostics: Use zim_health — with no argument it consolidates server health, configuration, and loaded-archives into a single response (three v1 tools in one call); pass a zim_file_path to validate one archive (Archive.check() + checksum + index/identity)
  3. Check logs: Look for error messages — error text is safe to copy verbatim (paths and PIDs are redacted)
  4. Test minimal case: Reproduce with simple setup

Where to get help#

  1. GitHub Issues: Report bugs or ask questions
  2. Documentation: Check other docs pages — FAQ, Architecture overview

Information to include#

When reporting issues, include:

  • Operating system and version
  • Python version: python --version
  • OpenZIM MCP version: python -c "import openzim_mcp; print(openzim_mcp.__version__)" or check zim_health.configuration.configuration.config_hash (the outer key is the block, the inner one the report) and the MCP serverInfo.version (reads from importlib.metadata; reports openzim-mcp’s actual version, not the MCP SDK default)
  • ZIM file details: Size, source, name
  • Error messages: Full error text
  • Configuration: MCP client config (remove sensitive paths)
  • Steps to reproduce: Exact commands used

Still having issues? Open an issue with detailed information about your problem.

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

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