[project] name = "openzim-mcp" version = "3.3.4" description = "OpenZIM MCP - ZIM MCP Server that enables AI models to access and search ZIM format knowledge bases offline" readme = "README.md" requires-python = ">=3.12" license = "MIT" authors = [ {name = "Cameron Rye", email = "c@meron.io"}, ] maintainers = [ {name = "Cameron Rye", email = "c@meron.io"}, ] keywords = [ "zim", "openzim", "mcp", "model-context-protocol", "ai", "llm", "knowledge-base", "offline", "wikipedia", "search", "libzim", ] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3 :: Only", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Text Processing :: Indexing", "Topic :: Internet :: WWW/HTTP :: Indexing/Search", "Topic :: Database", "Typing :: Typed", ] # M30: dependency upper bounds. The MCP SDK and Pydantic ship fast and have # repeatedly carried breaking changes across minor versions; cap each at the # next major so a fresh ``pip install openzim-mcp`` can't silently land on a # wheel-incompatible upstream. ``libzim`` is similarly tightly coupled to the # Python binding ABI. tiktoken caps a major because its BPE table updates # have shipped breaking changes across minors in the past. Beautifulsoup # stays unpinned-upward (very stable surface). dependencies = [ "beautifulsoup4>=4.14.3,<5.0", "html2text>=2025.4.15,<2027.0", "libzim>=3.9.0,<4.0", # One audited minor series at a time. Upstream's VERSIONING.md excludes # underscore-prefixed names and undocumented modules from its compatibility # promise, and this server depends on a spread of them: sdk_compat.py # patches the dicts under the method tables, server.py/subscriptions.py # install handlers through ``_request_handlers`` + ``HandlerEntry``, # resource_tools.py writes into ``_resource_manager._templates``, # http_app.py imports ``check_accept_headers`` / # ``MCP_SESSION_ID_HEADER`` / ``DEFAULT_MAX_REQUEST_BODY_SIZE``, and # mcp_envelope.py calls ``fn_metadata.convert_result``. CI resolves the # lockfile, but fresh installs (pip/uvx, Smithery, Docker) take the newest # wheel the range admits, so an unaudited minor would break those with the # suite green. # # The floor moves with the ceiling on purpose, so the declared range is # exactly the one series CI resolves and tests. 2.0.x is not merely # unaudited, it is behaviourally different: a crashing tool body's # exception text reaches the client verbatim (upstream #3314 landed in # 2.1.0), the SSE message endpoint answers a GET with 400 rather than 405, # and ``DEFAULT_MAX_REQUEST_BODY_SIZE`` has not yet moved to # ``transport_security`` — which makes building the HTTP app raise # ImportError, though stdio still starts. An ``except ImportError`` # fallback would restore the import and none of the rest, on a branch no # CI job runs. # # To raise the ceiling: re-audit the seams against the new wheel, run the # full suite AND ``pytest -m live``, re-measure the advertised schema # footprint, move the floor to the new series' first release, and ``uv # lock --upgrade-package mcp`` — plain ``uv lock`` keeps the old # resolution, so a widened cap otherwise ships a wheel CI never ran. # ``tests/test_sdk_seam_audit.py`` pins what that audit established, and # fails if the lockfile is left below the cap's top series. # # None of that retires the ping shim: 2026-07-28 defines no ping, so no SDK # release is going to supply those rows (see sdk_compat and issue #371). "mcp[cli]>=2.1.0,<2.2", "pydantic>=2.13.3,<3.0", "pydantic-settings>=2.14.0,<3.0", "tiktoken>=0.7.0,<1.0", ] [project.optional-dependencies] # Phase D sub-D-1: cross-encoder reranker via FastEmbed (ONNX-backed, # no torch dependency). Adds ~150 MB install footprint. Lazy-imported # inside openzim_mcp.ml.reranker; default install is unaffected. reranker = [ "fastembed>=0.4.0,<1.0", ] [project.urls] Homepage = "https://github.com/cameronrye/openzim-mcp" Repository = "https://github.com/cameronrye/openzim-mcp.git" Issues = "https://github.com/cameronrye/openzim-mcp/issues" Changelog = "https://github.com/cameronrye/openzim-mcp/blob/main/CHANGELOG.md" Documentation = "https://github.com/cameronrye/openzim-mcp#readme" "Security Policy" = "https://github.com/cameronrye/openzim-mcp/blob/main/SECURITY.md" "Contributing" = "https://github.com/cameronrye/openzim-mcp/blob/main/CONTRIBUTING.md" [project.scripts] openzim-mcp = "openzim_mcp.__main__:main" [build-system] requires = ["setuptools>=61"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] where = ["."] include = ["openzim_mcp*"] [tool.setuptools.package-data] openzim_mcp = ["data/*.txt", "data/*.toml", "tools/*.md"] [tool.uv] # pyjwt is pulled in transitively via mcp[cli] (which depends on pyjwt[crypto]). # Floor it at 2.13.0 so the release pip-audit gate stays clear of # PYSEC-2026-175 / -177 / -178 / -179 — four advisories against pyjwt 2.12.1, # all fixed in 2.13.0. mcp itself sets no upper bound, so this only raises the # floor. Drop this constraint once mcp's own pin guarantees >= 2.13.0. # # pip is pulled in transitively via pip-api (a pip-audit dependency); it is a # dev/tooling artifact, not a runtime dependency. Floor it at 26.1.2 so the # release pip-audit gate stays clear of PYSEC-2026-196 (an advisory against # pip 26.1, fixed in 26.1.2). Drop this constraint once pip-api's own pin # guarantees a patched pip. # # cryptography / python-multipart / starlette are pulled in transitively via # mcp[cli] (uvicorn + starlette for the HTTP/SSE transport; cryptography via # pyjwt[crypto]). Floor each at its first patched release so the pip-audit gate # stays clear of: GHSA-537c-gmf6-5ccf (cryptography < 48.0.1) and # CVE-2026-69247 (cryptography < 50.0.0); CVE-2026-53538 / # -53539 / -53540 (python-multipart < 0.0.31); and CVE-2026-48817 / -48818 / # -54282 / -54283 (starlette < 1.3.1). Floors only — mcp sets no upper bound; # drop each once mcp's own pins guarantee patched versions. # # soupsieve is pulled in transitively via beautifulsoup4 (its CSS selector # engine). Floor it at 2.8.4 so the pip-audit gate stays clear of # PYSEC-2026-3071 / -3072, two advisories against soupsieve 2.8.3. beautifulsoup4 # sets no upper bound, so this only raises the floor. Drop this constraint once # beautifulsoup4's own pin guarantees >= 2.8.4. constraint-dependencies = [ "pyjwt>=2.13.0", "pip>=26.1.2", "cryptography>=50.0.0", "python-multipart>=0.0.31", "starlette>=1.3.1", "soupsieve>=2.8.4", ] [tool.black] line-length = 88 target-version = ['py312'] [tool.isort] profile = "black" line_length = 88 [tool.mypy] python_version = "3.12" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true disallow_incomplete_defs = true [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] python_classes = ["Test*"] python_functions = ["test_*"] addopts = "--cov=openzim_mcp --cov-report=html --cov-report=term-missing --cov-report=xml -m 'not live'" [tool.coverage.run] source = ["openzim_mcp"] omit = ["tests/*"] [dependency-groups] dev = [ "black>=26.3.1", # Used by tests/live/ and by the in-process ASGI seam tests in # tests/test_sdk_seam_audit.py. Previously arrived transitively via mcp # 1.x; the v2 SDK ships httpx2 instead, so those suites fail to import # without an explicit declaration. Declared here rather than depended on # transitively so a future SDK dependency change can't break collection. "httpx>=0.28", "flake8>=7.3.0", "isort>=8.0.1", # Used by the dependency-range gates in tests/test_sdk_seam_audit.py and # tests/test_docs_freshness.py, which parse pyproject's own specifier # instead of pattern-matching it. Arrives transitively via black/pytest # today; declared for the same reason httpx is, so a tooling change # cannot break collection. "packaging>=25.0", # Capped below 3.0. The 2.x major was taken deliberately, as the old cap # comment asked: 2.3.1 type-checks the whole package clean under both the # Makefile config and the pre-commit hook's args, and the pre-commit rev # moves in lockstep with this floor. Keep crossing this cap by hand — # Dependabot is set to lockfile-only so it cannot widen the line itself. "mypy>=2.3.1,<3.0", "pytest>=9.0.3", "pytest-asyncio>=1.3.0", "pytest-benchmark>=5.2.3", "pytest-cov>=7.1.0", "pre-commit>=4.6.0", "bandit[toml]>=1.9.4", "bandit-sarif-formatter>=1.1.1", "pip-audit>=2.10.0", ] [tool.bandit] exclude_dirs = ["tests", "test_data", "scripts"] skips = ["B101"] # Skip assert_used test [tool.bandit.assert_used] skips = ["*_test.py", "test_*.py"]