Repository navigation
v1.38
The time zone release. Promised on #75 on 2026-10-02 as the first item of the next version; four patch releases went out in between without it, which is on us. Thank you, @G0nz0uk, for the report and the patience.
Added
- One time zone per call, used in both directions (#75). A UK operator asking for "the last hour of problems" at 20:00 BST got 18:00-19:00: Zabbix returns bare epochs, the server read a date without a zone as UTC, and nothing told the model what time it was where the operator sat. The server now resolves a zone for every tool call, first hit wins:
timezoneon the calling MCP token ([tokens.<id>]), then on the Zabbix entry ([zabbix.<name>]), then[server].timezone, then the zone of the Zabbix user behind the API token (user.checkAuthentication, unlessdefault), then the Zabbix frontend default (settings.getdefault_timezone, unlesssystem), then the zone of the host running the MCP server, then UTC. Values are IANA names,UTCorsystem; an unknown name is a config error on[server]/[zabbix.<name>]and a logged, ignored value on a token (a typo must not lock a client out). The Zabbix-side lookup is cached per server until a reconnect; a refusal ofsettings.getis cached too, a connection failure is not. With the zone in hand: every epoch field in a result (clock,r_clock,lastchange,lastclock,lastaccess,active_since,active_till,expires_at,start_date, ... the full list isEPOCH_FIELDSintimezones.py) gets a<field>_localsibling such as2026-04-01T09:00:00+01:00, with the epoch kept and0("never") left alone; the[System: ...]preamble line of each response carriesTime zone: Europe/London; now 2026-10-14T20:05:11+01:00. ..., so the model knows the current local time (one line as before, so a consumer stripping the first line keeps parsing; omitted withraw_json=true, the_localfields stay); the extension tools (problem_active_get,item_history_summary_get,infrastructure_summary_get, ...) get the same_localsiblings and note, and thetime/lastclock_humanfields ofproblem_active_getandhost_status_getrender in it;health_checkreportstimezoneandnow_localper server; and the three config fields are editable in the admin portal (server form, token create / edit, settings). OAuth clients (claude.ai, ChatGPT connectors) get a zone too: the portal user'stimezoneif set (new field on the user form,[admin.users.<name>].timezone), else the zone the browser reports on the consent page (Intl.DateTimeFormat().resolvedOptions().timeZone, empty without JavaScript); it rides on the authorization code, the access token and the refresh token next to the role, survives a refresh, and anything that is not a known zone resolves to none rather than break a call. The user's own zone counts as the operator's word and ranks first like a[tokens.X]zone; the browser-reported one is only a hint and ranks below[zabbix.<name>].timezoneand[server].timezone, so an operator who pins a server's zone keeps it for OAuth clients too. A<field>_localvalue echoed back into an update call (a model editing amaintenance_getresult and sending it tomaintenance_update) is dropped before the request, so Zabbix never sees the unknown parameter.
Changed
- The server knows which API method exists on which Zabbix release, and says so. The catalog is one list for every configured server, but a 5.0 box has no
token.*, a 6.0 box notemplategroup.*, a 6.4 box noproxygroup.*/connector.*/mfa.*/history.push, and 8.0 (docs are up, release is not) dropshost.massupdate,hostgroup.massupdate,template.massupdate,templategroup.massupdateandhostinterface.replacehostinterfaces. Until now a call to any of these came back as Zabbix's bareMethod not found, and the model had no way to pick the right tool for the right server.api/versions.pynow carries, for all 223 methods, the first release that has each one and the release that removed it, audited page by page against the 5.0 ... 7.4 and 8.0 documentation. With it: a call to a method the server does not have is refused up front with the reason (proxygroup.get requires Zabbix 7.0 or later (this server runs 6.0.30),host.massupdate was removed in Zabbix 8.0 (this server runs 8.0.0). Use host.update (per host) instead), through the dedicated tools andzabbix_raw_api_callalike;tools/listcarries a "Zabbix version note" on every tool that is missing on one of the servers the caller may use, naming the server and the reason, and leaves out a tool that exists on none of them (nothing is hidden while a server's version is still unknown);health_checkreportszabbix_versionandunavailable_toolsper server; and the versions are learned in a background thread at startup so the catalog is right from the firsttools/listwithout a Zabbix round trip inside it. Tool objects are copied per caller, never mutated. The five 8.0-removed tools say so in their descriptions as well. Other 8.0 findings from the same audit: new objectsapmlog,apmmetric,apmspan,apmtrace(.get),device(.get,.offboard),serverinfo.get, plusservice.*andapiinfo.versionwhich we never had - dedicated tools follow once 8.0 ships (reachable throughzabbix_raw_api_callmeanwhile); and on a new 8.0 installation API access is disabled by default for every role, so the API user behind the token needs it switched on. - A date-time sent without a zone is read in the resolved zone, no longer as UTC.
time_from = "2026-04-01 09:00:00"from a London client now means 09:00 BST. A value with an offset orZ, and a Unix epoch, are taken as they are. This applies on every path v1.37.4 made the conversion reach: typed parameters,extra_params, nested fields, list payloads andzabbix_raw_api_call. Pintimezone = "UTC"on a token, a Zabbix entry or[server]to keep the old reading (a browser-reported OAuth zone does not override those pins; a zone set on a token or portal user does). - The
<field>_localsiblings make bulk rows longer, so ahistory_get/problem_getanswer that hitsresponse_max_charsnow carries roughly a third fewer rows than v1.37.4 within the same budget (measured at the default 50000:history.get475 -> 326 rows,problem.get211 -> 175). Raiseresponse_max_charsor ask for narrower windows if a workflow depends on the old counts; the per-row local time is what the model reads, and the epoch is kept. - Versions are learned at startup in a daemon thread that connects outside the client lock, so a server that is black-holed (VPN down, firewall drop) stalls only its own warm-up, never the first call to a healthy server. A connection failure during the Zabbix-side zone lookup is never cached (the library reports it as
ProcessingError("Unable to connect ..."), which is now recognised); only an API-level refusal is. - The "token created" view of the admin portal no longer logs a JavaScript error for the scope bubbles it does not have (pre-existing, cosmetic).
- New dependency
tzdata(zone data for hosts without a system database).
Verified
- 643 unit + e2e tests, all passing. 39 in
tests/test_timezones.py: the chain level by level with a stand-in client manager, the Zabbix-side lookup (user zone,default-> frontend default,system-> nothing, refusal cached, connection failure not cached, reconnect drops the cache, legacy sessions usesessionid), zone-less input in London / Tokyo while explicit offsets and epochs stay put, the_localsiblings at depth without mutating the input and never for0,start_timeor a tag merely namedclock, the note line and its absence withraw_json, the reporter's scenario throughMCPServer.call_tool(zone reported by Zabbix as the user'sEurope/London, "from 09:00" sent as 08:00 UTC,clock_localback as 09:00 BST), the token zone overriding everything, the Zabbix-entry zone,zabbix_raw_api_call,health_check, and the config / token validation. 34 intests/test_admin_timezone.pydrive the real portal handlers: server create / edit, token create / edit / clear, settings, the user form, each write re-read through the config loader. 18 intests/test_oauth_timezone.pyandtests/test_oauth_timezone_e2e.py: the zone riding code -> access token -> refresh token through two rotations, the user's zone beating the browser's, an invalid browser zone ignored, and the real register / authorize / login / consent / token flow postingbrowser_timezone=Europe/Londonagainst a fake Zabbix endpoint, thenhost_getanswering withTime zone: Europe/Londonand still after a refresh. 15 intests/test_method_versions.py: the since / removed tables, refusals on an older release and on 8.0 through the real tools andzabbix_raw_api_call,tools/listleaving the catalog alone while versions are unknown, annotating on a 7.4 + 6.0 mix without mutating the shared tool objects, hiding a tool no reachable server has, a token bound to one server seeing only that server's notes,health_check, the warm-up surviving a server that is down and not holding the lock while one hangs, a tool staying listed while one server's version is still unknown, a browser-reported zone ranking below the operator's pins,_localsiblings stripped from an update payload, and a wrapped connection failure not being cached. One more for an extension tool getting the_localfields. An independent adversarial review of the whole diff ran before release; its findings (the warm-up lock, the cached connection failure, the hide-while-unknown case, the_localround trip, the OAuth precedence, the row-count effect) are the items above. The two modules from v1.37.4 pass with the zone pinned to UTC. - Same suite under
python:3.10-slimand under MCP SDK 2.3.0. Live with two servers at once (Zabbix 7.4.14 and 7.0.30):tools/listcarryingnot on 'rv70' - discoveryruleprototype.get requires Zabbix 7.4 or later (this server runs 7.0.30)on the fourdiscoveryruleprototype_*tools and nothing on the rest, the call refused on 7.0 and answered on 7.4, the same throughzabbix_raw_api_call,health_checklisting both versions and the four unavailable tools for 7.0. Live against Zabbix 7.4:health_checkreporting the London token's zone and local time,trigger_getreturninglastchange_localin BST,maintenance_createwith2026-11-01 09:00:00and a nestedstart_datestored as 09:00 GMT, and the smoke user's session tools. CRUD smoke; installer matrix 18/18;install.sh updatefrom v1.37.4 to this version on a clean container (config preserved).