You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.