Imported from velesnitski/zbbx-mcp (
AGENTS.md). Install upstream withnpx skills add velesnitski/zbbx-mcp. Copyright stays with the author.
AGENTS.md
This file provides guidance to WARP (warp.dev) when working with code in this repository.
Commands
Setup (development):
uv pip install -e ".[test]"
# or with pip:
pip install -e ".[test]"
Run all tests (~2s):
uv run pytest
Run a specific test file or test by name:
uv run pytest tests/test_registration.py
uv run pytest tests/test_server.py
uv run pytest -k "test_name"
Test server startup without a real Zabbix instance:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"},"protocolVersion":"2024-11-05"}}' \
| ZABBIX_URL="https://test.zabbix.example.com" ZABBIX_TOKEN="test-token" python -m zbbx_mcp.server
No linting or type checking is configured. The test suite uses pytest with asyncio_mode = "auto".
Architecture
Overview
This is a Model Context Protocol (MCP) server that exposes Zabbix monitoring data as tools callable by LLMs. It uses the FastMCP class from the mcp library as the server framework. Python 3.10+, async httpx HTTP/2 client.
Key source modules
server.py— entry point,create_server()factoryclient.py—ZabbixClientwrapping Zabbix JSON-RPC APIconfig.py— env var loading, multi-instance configresolver.py—InstanceResolverroutes calls to the rightZabbixClientdata.py— shared data fetching pipeline,ServerRow,extract_country(), metric key constantsclassify.py— standalone host classification (notools/imports — avoids circular import)rollback.py—RollbackLogdeque +SNAPSHOT_CONFIGlogging.py— structured JSON error log + analytics log,logged()decorator, Sentry integrationformatters.py— shared Zabbix entity formatters (format_host_list,format_problem_list, etc.)utils.py—format_results(),resolve_group_ids(),ROLLBACK_STRIP_FIELDSexcel.py— Excel workbook generation forgenerate_full_reporttools/*.py— each file exports a singleregister(mcp, resolver, skip)functiontools/__init__.py—register_all(),WRITE_TOOLSset
Startup flow
create_server() → load_all_configs() → one ZabbixClient per instance → InstanceResolver → register_all() → wraps every tool with logged() (analytics) and _compress_response() (token budget).
Tool registration pattern
Every file under tools/ exports register(mcp, resolver, skip). Each tool is conditionally added as @mcp.tool() async function gated by if "tool_name" not in skip. The skip set is built from WRITE_TOOLS (when ZABBIX_READ_ONLY=true) and DISABLED_TOOLS.
Adding a new tool
- Add
@mcp.tool()async function insideregister()in the appropriatetools/*.pyfile - Gate with
if "tool_name" not in skip: - If the tool mutates data, add it to
WRITE_TOOLSintools/__init__.py - Add the tool name to
EXPECTED_TOOLSintests/test_registration.py - Update the tool count assertion in
tests/test_server.py - Update the tool table in
README.md - Run
uv run pytest— all tests must pass
ZabbixClient (client.py)
Wraps Zabbix's JSON-RPC API via httpx.AsyncClient with HTTP/2 and a connection pool. Key methods:
call(method, params)— single JSON-RPC callcall_many(calls)— parallel calls viaasyncio.gathersnapshot_and_record(action, object_type, object_id)— fetches current object state before a mutation and records it in theRollbackLogrecord_create(object_type, object_id)— records creates without a snapshot- TTL cache (
_get_cached/_set_cache) used for the "all enabled hosts" query in report modules
Rollback system (rollback.py)
Each ZabbixClient holds its own RollbackLog (bounded deque, max 50 entries). Before any write (update/delete), the tool calls snapshot_and_record() to capture prior state. SNAPSHOT_CONFIG maps object types to their API methods. rollback_last and rollback_by_index tools undo operations.
Report data pipeline (data.py)
fetch_all_data() fetches dashboards and hosts in parallel (phase 1), fires ~13 item.get calls simultaneously (phase 2) for CPU, memory, traffic, macros, and templates. Missing traffic data is filled with a fallback name-based search (phase 3). Results are assembled into ServerRow dataclasses and sorted (dashboard hosts first).
Multi-instance support
config.py reads ZABBIX_INSTANCES=prod,staging and loads per-instance env vars (ZABBIX_PROD_URL, ZABBIX_PROD_TOKEN, etc.). The first instance falls back to unprefixed ZABBIX_URL/ZABBIX_TOKEN. InstanceResolver.resolve(instance) picks the right client.
Key environment variables
ZABBIX_URL/ZABBIX_TOKEN— requiredZABBIX_READ_ONLY=true— blocks all tools inWRITE_TOOLSDISABLED_TOOLS=foo,bar— removes specific tools entirelyZABBIX_PRODUCT_MAP— JSON file or inline JSON mapping host group names to[product, tier]ZABBIX_COMPACT=true— strips markdown from responses (~40% token savings)ZABBIX_RESPONSE_BUDGET=6000— max chars per tool response (0 = unlimited)ZABBIX_COMPACT_TOOLS=true(default on) — stripsArgs:sections from tool docstrings at runtimeZABBIX_ALLOW_HTTP=1— allows non-HTTPS URLsZABBIX_INSTANCES=prod,staging— enables multi-instance mode
Test notes
test_registration.py— fast unit test; updateEXPECTED_TOOLSand run this when adding/removing toolstest_server.py— spawns the real server as a subprocess and sends JSON-RPC messages over stdio; asserts exactly 121 tools are registered- Tests use
unittest.mock.patch.dict(os.environ, ..., clear=True)to isolate config loading from the ambient environment
Rules
- Never commit
tasks.md— it's in.gitignoreand contains internal planning notes - No sensitive data in code or git — no real hostnames, company names, product names, or server naming patterns; use generic examples like
srv-nl01,srv-us01in docstrings and tests - No hardcoded service identifiers — use generic labels (Primary, Secondary, Tertiary) in public output
- Country filter — always use
extract_country(hostname)for exact 2-letter match; never use substringcountry in hostname - Compact by default — tools should return concise output; use
max_resultswith sensible defaults, group repetitive entries, show omitted count - Error handling — catch
(httpx.HTTPError, ValueError), return a user-friendly string, never raise - Token budget — responses are truncated by
ZABBIX_RESPONSE_BUDGET(default 6000 chars); design output to fit classify.pymust not import fromtools/— circular import risk- Args in docstrings — keep
Args:blocks in code even thoughZABBIX_COMPACT_TOOLS=truestrips them at runtime; the JSON schema still needs them
Branching
dev— active development branchmain— stable, always fast-forward merged fromdev- Always confirm before
git push
Code style
- No linter configured — follow existing patterns
- All tool implementations must be async functions with type hints on signatures
- Minimal docstrings: tool description + Args block (no Returns/Raises sections)