Skip to content

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 07:47
· 15 commits to main since this release

[1.5.0] - 2026-11-02

What changed

  • Read-only tool results were unbounded. A single call could return hundreds of kilobytes of compact JSON — the bundled cloud-provider catalogue alone ships ~700 KB of IP ranges — which blows through a client's context window and, worse, is what an agent actually ends up reasoning over. A new npg_mcp/resultcap.py caps every read-only result at NPG_MAX_RESULT_CHARS (default 65536 compact-JSON characters), applied post-registration in main() right after toolsets.annotate_tools so validation still runs against each tool's real signature.
  • The reduction is boundary-only: trailing array items are dropped (binary-searched against the array's own serialized size, so one measure per round), oversized string values are clamped with an ... [clamped N chars] marker across a falling budget ladder, and JSON text is truncated only as a last resort. Nothing is ever cut mid-structure or mid-string, so what a client receives is always complete, parseable JSON. Under-cap results are returned byte-for-byte identical with no truncated key.
  • Over-cap results gain a sibling truncated object — total_chars, returned_chars, omitted_items, clamped_strings, and a hint derived from the tool's own signature (it names the exact narrower parameters that tool really has: limit/offset/sections/lines, or says it has no paging parameters, and always offers expand=true). The two base64 payload tools (npg_download_backup, npg_get_certificate_download) additionally warn that a shortened payload cannot be decoded.
  • npg_list_cloud_providers defaulted to the full upstream payload with every CIDR inline. It now projects each provider to id/slug/name/region/enabled plus a derived ip_range_count, and clips description to 160 chars — a 2000-CIDR provider drops 38 KB → 338 B. compact=false returns the untouched upstream objects byte-for-byte (verified by live equality against a raw GET of the same endpoint), so nothing is renamed, re-ordered or lost on that path. Compaction runs before slicing, so paging metadata still reports the true total.
  • Warning/validation blocks were repeated verbatim across sibling tools, so the same 300–400 character paragraph existed in four to eight descriptions at once — inflating every tools/list response and creating drift risk when one copy was edited. Seven shared blocks (confirm-DNS re-lookup and 400/403/409 handling, paginated-list paging metadata/zero-arg, MCP-side limit/offset slicing, log-autocomplete q/limit filtering, bulk per-item failure isolation, strict kwargs validation, global partial-update omitted-field semantics) now live only in a canonical tool, with the 29 registered siblings carrying a short pointer ("... same as npg_update_settings").
  • A new tests/test_description_cross_refs.py guard pins the dedup: for each SHARED_BLOCKS entry the canonical tool must still carry the block, every listed sibling must cross-reference instead of repeating, no unlisted tool may start repeating the block again, and no description may reference a tool name that does not exist (0 dangling npg_* refs).
  • tests/test_result_cap.py (400 lines) pins the cap end-to-end: env parsing and clamping, every reduction path, the derived hint, expand lifting the cap, the exempt-tool path, idempotent double instrumentation, and two coverage gates — every readOnlyHint tool must declare expand: bool = False, and no mutating tool may grow one.
  • Verified on the isolated test stack against a HEAD-pinned local image (local/npg-mcp:a42d22c@sha256:07b633e7…, image and checkout file hashes identical), not the stale published image: 309 tools served with all 309 descriptions byte-identical to the checkout, all 7 shared blocks present only in their canonical tools with all 29 siblings pointing at them, exactly 5 short repeated sentences (the intended pointers), 0 dangling refs, every REQUIRED: param still named, 39 live read calls across families green, and the three documented validation paths behaving as described.
  • Release gates on the release commit: 309 tools with 0 duplicates and 0 tool-name changes, ruff check . clean, mypy npg_mcp/ clean, pytest 591 passed, docs --check exit 0 (460-char description cap, longest 460), tier counts match tier_allowed() (read=137 / standard=262 / full=309).

What's new

  • npg_get_stack_features — a new zero-argument, report-only, read-tier tool that tells an agent what the deployed NPG release actually serves before it plans a workflow. Ten optional feature families are probed with one GET each: Cloudflare-tunnel state/connections, global fail2ban, global WAF, challenge, GeoIP (flags), plus counts of SSO providers, DNS providers, cloud providers, DDNS records and notification channels.
  • Per-family isolation is the tool's core contract: each probe runs in its own try/except, so a permission-scoped API token (403), a family absent from the bridged release (404) or a transport failure comes back as readable: false + http_status as report data, never as a failed tool call. It returns families_probed/families_readable/unreadable_families and report_only: true, and it deliberately does not hide, filter or gate any other tool — the surface is identical whether or not the probed families are configured.
  • NPG_MAX_RESULT_CHARS (new env var, default 65536, clamped to [1024, 8000000], invalid values fall back with a warning) with a per-call expand: bool = False escape hatch on all 141 capped read-only tools — one call returns the complete payload. npg_download_log_file keeps its own 256 KB cap instead of the generic one.
  • compact: bool = True on npg_list_cloud_providers — the built-in catalogue is now cheap by default (derived ip_range_count instead of the CIDR sets), with compact=false for full fidelity. Same route, same tool, same tier: one switch rather than a second tool.
  • Tool surface: 309 tools (was 308) with the new stack-features probe; tiers move to read=137 / standard=262 / full=309 (each +1).

Breaking changes

  • Read-only tool results larger than NPG_MAX_RESULT_CHARS (default 65536 compact-JSON characters) are now truncated by default. Callers that relied on receiving a complete payload in one call must pass expand=true (or page with the parameters named in the truncated.hint). Under-cap results are unchanged.
  • The default response of npg_list_cloud_providers no longer contains the full ip_ranges CIDR sets, created_at/updated_at and the untruncated description — each row now carries id/slug/name/region/enabled + ip_range_count and a 160-char description. Pass compact=false for the previous byte-for-byte payload, or npg_get_cloud_provider for one provider in full.
  • No tool was removed or renamed and no parameter was renamed or retyped; the only signature change is the additive expand parameter on read-only tools and the additive compact parameter on npg_list_cloud_providers. No auth or config requirement changed — NPG_MAX_RESULT_CHARS is optional and defaults preserve prior behaviour for small payloads.