Skip to content

v1.38

Choose a tag to compare

@hermanekt hermanekt released this 14 Oct 22:02
· 3 commits to main since this release
e6112d5

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: timezone on 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, unless default), then the Zabbix frontend default (settings.get default_timezone, unless system), then the zone of the host running the MCP server, then UTC. Values are IANA names, UTC or system; 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 of settings.get is 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 is EPOCH_FIELDS in timezones.py) gets a <field>_local sibling such as 2026-04-01T09:00:00+01:00, with the epoch kept and 0 ("never") left alone; the [System: ...] preamble line of each response carries Time 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 with raw_json=true, the _local fields stay); the extension tools (problem_active_get, item_history_summary_get, infrastructure_summary_get, ...) get the same _local siblings and note, and the time / lastclock_human fields of problem_active_get and host_status_get render in it; health_check reports timezone and now_local per 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's timezone if 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>].timezone and [server].timezone, so an operator who pins a server's zone keeps it for OAuth clients too. A <field>_local value echoed back into an update call (a model editing a maintenance_get result and sending it to maintenance_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 no templategroup.*, a 6.4 box no proxygroup.* / connector.* / mfa.* / history.push, and 8.0 (docs are up, release is not) drops host.massupdate, hostgroup.massupdate, template.massupdate, templategroup.massupdate and hostinterface.replacehostinterfaces. Until now a call to any of these came back as Zabbix's bare Method not found, and the model had no way to pick the right tool for the right server. api/versions.py now 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 and zabbix_raw_api_call alike; tools/list carries 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_check reports zabbix_version and unavailable_tools per server; and the versions are learned in a background thread at startup so the catalog is right from the first tools/list without 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 objects apmlog, apmmetric, apmspan, apmtrace (.get), device (.get, .offboard), serverinfo.get, plus service.* and apiinfo.version which we never had - dedicated tools follow once 8.0 ships (reachable through zabbix_raw_api_call meanwhile); 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 or Z, 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 and zabbix_raw_api_call. Pin timezone = "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>_local siblings make bulk rows longer, so a history_get / problem_get answer that hits response_max_chars now carries roughly a third fewer rows than v1.37.4 within the same budget (measured at the default 50000: history.get 475 -> 326 rows, problem.get 211 -> 175). Raise response_max_chars or 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 use sessionid), zone-less input in London / Tokyo while explicit offsets and epochs stay put, the _local siblings at depth without mutating the input and never for 0, start_time or a tag merely named clock, the note line and its absence with raw_json, the reporter's scenario through MCPServer.call_tool (zone reported by Zabbix as the user's Europe/London, "from 09:00" sent as 08:00 UTC, clock_local back 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 in tests/test_admin_timezone.py drive the real portal handlers: server create / edit, token create / edit / clear, settings, the user form, each write re-read through the config loader. 18 in tests/test_oauth_timezone.py and tests/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 posting browser_timezone=Europe/London against a fake Zabbix endpoint, then host_get answering with Time zone: Europe/London and still after a refresh. 15 in tests/test_method_versions.py: the since / removed tables, refusals on an older release and on 8.0 through the real tools and zabbix_raw_api_call, tools/list leaving 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, _local siblings stripped from an update payload, and a wrapped connection failure not being cached. One more for an extension tool getting the _local fields. 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 _local round 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-slim and under MCP SDK 2.3.0. Live with two servers at once (Zabbix 7.4.14 and 7.0.30): tools/list carrying not on 'rv70' - discoveryruleprototype.get requires Zabbix 7.4 or later (this server runs 7.0.30) on the four discoveryruleprototype_* tools and nothing on the rest, the call refused on 7.0 and answered on 7.4, the same through zabbix_raw_api_call, health_check listing both versions and the four unavailable tools for 7.0. Live against Zabbix 7.4: health_check reporting the London token's zone and local time, trigger_get returning lastchange_local in BST, maintenance_create with 2026-11-01 09:00:00 and a nested start_date stored as 09:00 GMT, and the smoke user's session tools. CRUD smoke; installer matrix 18/18; install.sh update from v1.37.4 to this version on a clean container (config preserved).