Skip to content

Releases: four2mis/npg-mcp

v1.7.1

Choose a tag to compare

@github-actions github-actions released this 03 Nov 21:13

[1.7.1] - 2026-11-04

What changed

  • The committed swagger.yaml was still the NPG 2.65.4 spec while the code and tool descriptions already targeted 2.66.0, so any swagger-driven gap analysis against this repo under-reported the release's upstream changes (the four /traffic/* endpoints and the new settings/credential fields were invisible in the committed spec). It is now synced from upstream tag v2.66.0: +232/-5 lines covering the GET /traffic/{summary,timeseries,realtime,export} paths with their Traffic* schemas, GlobalSettings.worker_shutdown_timeout, Route53Credentials.use_default_credentials plus the masked response field, the SSO group_claim maxLength: 64, and the challenge/dashboard documentation clarifications. Documentation artefact only — no runtime code path reads it.
  • Three DNS-provider tool descriptions now document the route53 use_default_credentials credential shape ({use_default_credentials: true} = AWS SDK default credential chain, no static keys; sending keys alongside it is a 400), which upstream added in v2.66.0 and which the tools accepted but never advertised: npg_create_dns_provider, npg_update_dns_provider, npg_test_dns_provider. npg_create_dns_provider's description was additionally rewritten in the standard imperative / REQUIRED: style used by the rest of the surface, with the per-provider credential shapes (cloudflare {api_token}, ionos {api_key_prefix, api_key}, route53 key pair or default chain) spelled out. Descriptions stay under the 460-char DESC_LEN cap (438 / 288 / 447 served chars).
  • Tool signatures are untouched by the description work, so no parameter, type, default or tier moved: the surface remains 313 tools (read 141 / standard 266 / full 313, destructive 47) and the CI description-sync gate sees no signature change.
  • Verified at this commit against the disposable test stack with a HEAD-pinned local image (local/npg-mcp:8437722, container main.py/toolsets.py sha256 identical to the checkout — the stale published npg-test-mcp image was deliberately not used as evidence): 42/42 live MCP checks pass, including route53 create with use_default_credentials → 201 + persisted true, the contradictory-keys 400 (route53 takes no access key with use_default_credentials), update/get/delete round-trip, credential masking, and 10 unrelated tools showing no regressions.
  • Release gates on this commit: ruff check . clean, mypy npg_mcp/ clean, pytest 621 passed, regenerate_all_docs.py --check exit 0 (tool-schemas/README/README.ko in sync at 313 tools, all prose rules and DESC_LEN pass), and .env.example tier counts match tier_allowed().

What's new

  • (none) — no new tools, endpoints or parameters; the exposed surface is unchanged at 313 tools. The swagger.yaml bundled in the repo now describes the same upstream NPG v2.66.0 API the tools are written against.

Breaking changes

  • (none) — documentation- and description-only release: no tool renamed, added or removed, no parameter retyped, no default changed, no tier-membership change.

v1.7.0

Choose a tag to compare

@github-actions github-actions released this 03 Nov 09:13

[1.7.0] - 2026-11-03

What changed

  • npg_update_settings could not set worker_shutdown_timeout, the nginx setting NPG v2.66.0 adds (seconds an old worker waits for its in-flight connections before exiting; 0 = wait for every connection, max 86400, omitted = 240). The field is part of upstream UpdateGlobalSettingsRequest, but it was missing from TOOL_KWARGS_WHITELIST in npg_mcp/upstream_models.py, so the client-side strict check rejected the call with ValueError: npg_update_settings: unknown field(s) ['worker_shutdown_timeout'] before any HTTP request — the only workaround was strict=false, i.e. exactly the silent-noop risk the whitelist exists to prevent. The key is now whitelisted (with a # v2.66.0 note) and npg_update_settings's description= names the field, its range and the 0 semantics in the same commit.
  • A regression test (tests/test_kwargs_whitelist.py::test_global_settings_v2_66_worker_shutdown_timeout_accepted) mirrors the existing v2.51 whitelist case and pins both 240 and the meaningful 0 value, including a mixed dict with keepalive_timeout.
  • Four new upstream GET /api/v1/traffic/* endpoints (NPG v2.66.0's per-host traffic analytics) had zero MCP coverage. Bridged as read-only tools in a new # ── Traffic ── section right after # ── Dashboard ──, all requiring dashboard:read: npg_get_traffic_summary (response bytes + requests per proxy host with each host's percentage, start/end, optional proxy_host_id via the timeseries/export siblings), npg_get_traffic_timeseries (start/end/proxy_host_id — host UUID, the literal unclassified, or omit — bucketed at 5min/hour/day), npg_get_traffic_realtime (fixed last 3 minutes in 10s buckets plus 30s average rates; deliberately no start/end), and npg_get_traffic_export (CSV).
  • npg_get_traffic_export calls c.get_text(...), not c.get(...): the route serves text/csv and client.get() unconditionally calls resp.json(), which would raise on a non-JSON body. It returns {"success": True, "data": "<csv text>"} following the npg_download_log_file / npg_get_metrics precedent, and its description says the payload is CSV, not JSON.
  • All four tools declare expand: bool = False (so the shared result cap applies and expand=true lifts it — enforced by tests/test_result_cap.py's read-only drift guard) and end their descriptions with the standard cap hint; every description stays under the 460-char DESC_LEN cap.
  • Description drifts left by v2.66.0 behaviour were corrected: npg_verify_challenge now documents 404 = proxy_host_id not found (the endpoint used to answer 500 for an unknown host), npg_create_sso_provider / npg_update_sso_provider document group_claim as max 64 chars (longer is now a 400 instead of a 500), and npg_get_dashboard_stats now states that without proxy_host_id only global rollup rows come back while an ID returns only that host's rows.
  • Generated artefacts were refreshed in the same commit: npg_mcp/permission_map.py regenerated from NPG's route registry (the four /traffic/* routes map to dashboard:read; 313 tools), tool-schemas.yaml + README.md + README.ko.md regenerated by regenerate_all_docs.py, and the new Traffic section registered with that generator's category map so the READMEs gain a Traffic | 4 tools row (29 categories).
  • Tier counts moved with the four read-only tools: read 137 → 141, standard 262 → 266, full 309 → 313 (destructive unchanged at 47), updated in .env.example, both READMEs and the tests/test_toolsets.py / tests/test_result_cap.py / tests/test_health.py pins.
  • Verified end to end against the local test stack (NPG v2.66.0 API) with a HEAD-pinned local image — local/npg-mcp:69933f957b9fd09379c42ab8b87214c30f8355be, not the stale published test image: 313 tools served, all four traffic tools returning live data (summary 288 bytes / 8 requests, timeseries hour point, realtime 10s buckets, CSV export containing the probe host) after driving requests through npg-test-proxy, worker_shutdown_timeout=120 accepted and reflected by npg_get_settings then restored to 0 (out-of-range 99999 rejected upstream with the documented 400), npg_verify_challenge surfacing the new 404 for an unknown host, and an SSO create/delete round-trip with a 64-char group_claim plus the 65-char 400 path. 23/23 live checks passed.
  • Release gates on this commit: 313 tools with no name changes, tier counts match tier_allowed() (read=141 / standard=266 / full=313, destructive 47), ruff check . clean, mypy npg_mcp/ clean, pytest 621 passed, docs --check exit 0 (460-char cap, longest 460), permission map in sync at 313 tools.

What's new

  • npg_get_traffic_summary — per-proxy-host traffic totals (response bytes, requests, percentage of the window total) for the last 24 h by default or an explicit RFC3339 window.
  • npg_get_traffic_timeseries — the bucketed traffic series (bytes + requests per point, resolution 5min/hour/day) for all hosts, one host UUID, or the literal unclassified bucket.
  • npg_get_traffic_realtime — live last-3-minutes traffic in 10s buckets with 30s average rates.
  • npg_get_traffic_export — the same per-host figures as CSV text for spreadsheets.
  • worker_shutdown_timeout is now settable through npg_update_settings (0 = wait for every connection, max 86400).

Breaking changes

  • (none) — all four tools are additive read-only GETs; no tool was renamed, retyped or removed, no parameter changed type, and no default changed. npg_update_settings accepts one previously-rejected kwargs key, so calls that used to fail client-side now reach the API. Deployments that pin NPG_TOOL_LEVEL see a larger surface (read 141, standard 266, full 313) because the new tools are read-tier.

v1.6.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 09:05

[1.6.0] - 2026-11-02

What changed

  • A deployment could only narrow its tool surface by tier (NPG_TOOL_LEVEL), which knows nothing about the API token it actually holds — a monitoring token scoped to reads still received every write tool it would only ever get 403s from, wasting context and inviting dead-end tool calls. A new npg_mcp/scopefilter.py adds opt-in scope-aware filtering (NPG_TOOL_SCOPE_FILTER, default off): when enabled, the server exposes only the tools the configured NPG_API_TOKEN can actually call, i.e. the tier manifest intersected with the token's grant. It is applied in main() immediately after toolsets.configure_toolset, so the exposed count reported by /health is the narrowed one and a hidden tool is never listed or callable.
  • The grant is resolved once at boot, from two endpoints in order: GET /api/v1/api-tokens (needs the literal apitoken:read) matching the row whose token_prefix the configured token starts with, with a fallback to GET /api/v1/auth/me, whose effective_permissions is what NPG enforces per request. The fallback is not cosmetic — a UI-created token can only carry * or one of the 15 legacy scopes, and NPG checks token scopes by exact string, so a scoped token always 403s on the token list; without the fallback the filter could only ever fail open.
  • Scope matching mirrors upstream APIToken.HasPermission exactly: a tool is exposed only when every permission its routes need is covered by *, the identical string, or an area:* family wildcard. Legacy scopes are not widened into other areas and write does not imply read — that expansion applies to roles, not tokens (verified live: a read_only token reaches proxy/certificate/waf/logs/settings/backup reads and 403s on redirect/access/authprovider/dnsprovider/ddns/audit/dashboard/user/role).
  • It fails open on every uncertainty, because hiding a tool that would have worked is worse than showing one that returns 403 — a hidden npg_create_* removes the only path that makes a feature exist. Both introspection endpoints failing (403/timeout/transport error), no row matching the token prefix, more than one matching row (ambiguity), an unset NPG_API_TOKEN, or an empty/unparseable permissions array all leave the full tier manifest intact, log at WARNING, and record the reason in hidden_by_scope.reason. Tools carrying no permission annotation are never hidden, so a newly bridged upstream endpoint cannot disappear silently.
  • Because the snapshot is taken at boot, changing the token's scopes in the NPG UI requires an MCP restart to change the surface — documented rather than papered over with a per-request re-check, which would have put an API round-trip in front of every tools/list.
  • The tool → permission map is generated, not hand-maintained: npg_mcp/permission_map.py (309 tools) is derived by scripts/derive_tool_permissions.py from NPG's own route-authorisation registry (api/internal/middleware/permissions.go), chosen because only 26 of the 318 swagger operations carry permission annotations. The five runtime-composed tools whose routes the registry cannot see are reviewed in MANUAL_OVERRIDES with a reason each, and the generator reports stale override entries.
  • npg_get_stack_features now reports what the filter did: hidden_by_scope carries enabled/applied/reason/snapshot/source/token_prefix/stored_permissions/granted_scope_count/hidden_count/hidden_tools/exposed_count/manifest_before/unmapped_tool_count, and is null when the filter is off. The tool is mapped to zero permissions so the filter can never hide the one tool that reports on the filter, and its description= was updated in the same commit to name the new field.
  • tests/test_scopefilter.py (29 tests) pins the behaviour: scope-matching semantics including the exact-string and area:* cases, every fail-open path, the tier ∩ grant intersection, the hidden_by_scope payload, the zero-permission exemption for the reporting tool, and two invariants — every registered tool must have a permission-map entry (so the map cannot fall behind the tool surface) and every mapped permission must be one NPG actually defines.
  • Documented in the same commit: NPG_TOOL_SCOPE_FILTER added to the env tables in README.md/README.ko.md and to .env.example (with the boot-snapshot and fail-open rules), a new "Scope Filtering" README section explaining the two introspection paths, the exact-match semantics and the boot-restart requirement, plus the two new modules in the project-structure listing.
  • Verified on the isolated test stack against a HEAD-pinned local image (local/npg-mcp:307e8a7@sha256:cd895e0d…, image and checkout file hashes proven identical; the published npg-test-mcp image was explicitly not used as evidence), 37/37 checks: filter off = 309 tools with all annotations, filter on with a * token = 309 identical, read_only token = 114, proxy:read token = 28, read tier ∩ grant = 106, unset and invalid token fail open to 309, a scope-hidden tool answering Unknown tool with zero side effects, hidden_by_scope listing exactly the 195 hidden tools, plus a full create→get→update→delete round-trip and 24 regression reads. Test stack left pristine; prod never touched.
  • Release gates on this commit: 309 tools with 0 duplicates and 0 tool-name changes, tier counts unchanged (read=137 / standard=262 / full=309), ruff check . clean, mypy npg_mcp/ clean, pytest 620 passed, docs --check exit 0 (460-char description cap), and the permission-map drift check in sync at 309 tools.

What's new

  • NPG_TOOL_SCOPE_FILTER — new env var (default 0, off; truthy 1/true/yes/on) that narrows the exposed tool surface to what the configured API token can actually call, on top of NPG_TOOL_LEVEL. Off by default, in which case the manifest, tool count and tiers are exactly as before.
  • npg_mcp/permission_map.py — generated tool → NPG permission map covering all 309 tools, and scripts/derive_tool_permissions.py — its generator, which reads NPG's route registry, applies the reviewed MANUAL_OVERRIDES, and fails on stale overrides or unmapped tools.
  • hidden_by_scope on npg_get_stack_features — the boot snapshot as data (token prefix, granted scopes, the exact hidden tool names, and why the filter failed open if it did), so a restricted server can always report what it hid.
  • No new tools: the surface stays at 309 tools with the same names, and the tiers are unchanged at read=137 / standard=262 / full=309.

Breaking changes

  • (none) — the filter is off by default and changes nothing when off: no tool or parameter was renamed, retyped or removed, no default or auth/config requirement changed, and the tier counts are unchanged. npg_get_stack_features gains an additive hidden_by_scope key (always null while the filter is off). Deployments that enable NPG_TOOL_SCOPE_FILTER do accept a narrower surface by design, and must restart the MCP container after changing the token's scopes in the NPG UI.

v1.5.0

Choose a tag to compare

@github-actions github-actions released this 02 Nov 07:47

[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.

v1.4.5

Choose a tag to compare

@github-actions github-actions released this 02 Nov 05:42

[1.4.5] - 2026-11-02

What changed

  • NPG_TOOL_LEVEL=read selected tools by name prefix alone (^npg_(get|list|view|download|check|detect)), and that prefix is a naming convention rather than proof of behaviour. Three probing tools matched it while issuing a POST or writing to the instance, so an agent deployed with the "must not mutate NPG state" tier could still mutate state (or ship a credential to a third party). npg_mcp/toolsets.py now carries a documented _READ_TIER_DENYLIST that tier_allowed() consults, so the read tier exposes 136 tools instead of 139.
  • npg_check_log_archive is now excluded from the read tier: its body is POST /system-settings/log-files/archive/check, which runs a write test (mount type, size, marker, probe) against the archive directory.
  • npg_detect_telegram_chats is now excluded from the read tier: its body is POST /notification-channels/detect-telegram-chats, an outbound call to the Telegram Bot API carrying the caller's bot token.
  • npg_check_backup_restore is now excluded from the read tier: it is GET-routed but creates a throwaway backup row, lists it, then deletes it — a transient DB write the verb check cannot see. This closes the gap where _READONLY_DENYLIST covered the annotation but tier_allowed() never consulted it.
  • Excluding was chosen over renaming: a rename is a breaking change for every MCP client and for the hundreds of tool descriptions that cross-reference tool names by exact string.
  • The two denylists are now explicitly distinct and documented as such — _READ_TIER_DENYLIST governs tier exposure, _READONLY_DENYLIST governs the readOnlyHint annotation.
  • Tier counts propagated in the same commit: .env.example's NPG_TOOL_LEVEL comment block and the env-var row plus Toolset Levels table in both README.md and README.ko.md now read 136 (was 139); standard (261) and full (308) are unchanged.
  • 4 new regression tests in tests/test_toolsets.py: every denylist entry must match the read-name regex and exist in main.py, each entry must be absent at read but present at standard/full, and test_read_tier_has_no_mutating_verbs now fails any future check_*/detect_* tool whose body issues a mutating verb — the class of bug this release fixes cannot silently return.
  • Verified on the isolated test stack against a HEAD-pinned local image (local/npg-mcp:e2f7ff8e@sha256:96d5f316…, container main.py/toolsets.py sha256 identical to the checkout), not the stale published image, using fresh verify-* fixtures: read = 136 with all three tools absent from tools/list and answering Unknown tool on call, standard = 261 and full = 308 with all three present and callable (archive check status=ready, backup-restore status=passed, telegram detect → structured upstream 400), readOnlyHint=false for all three, and 5 unrelated read tools unaffected. Edge cases covered: upper-cased and invalid NPG_TOOL_LEVEL fall back to 136/308 as documented.
  • Release gates on the release commit: 308 tools with 0 duplicates, ruff check . clean, mypy npg_mcp/ clean, pytest 508 passed, docs --check exit 0 (460-char description cap, longest npg_bulk_import_proxy_hosts at 460), tier counts match tier_allowed() (read=136 / standard=261 / full=308).

What's new

  • The read toolset tier now actually honours its documented contract: it contains no tool that writes to NPG or performs an outbound credentialed call, and a regression test enforces that for any check_*/detect_* tool added in future.
  • _READ_TIER_DENYLIST is a new, self-documenting extension point in npg_mcp/toolsets.py — each entry carries the reason it is excluded, so the next check_*/detect_* probe with a mutating body only needs one documented line.
  • No new tools: the surface stays at 308 tools with the same names.

Breaking changes

  • At NPG_TOOL_LEVEL=read, three tools are no longer exposed or callable: npg_check_log_archive, npg_detect_telegram_chats, npg_check_backup_restore. Read-only deployments that relied on them must raise NPG_TOOL_LEVEL to standard or full. Nothing else changes — no tool or parameter was renamed or removed, no default, auth or config requirement changed, and standard (261) and full (308) are unchanged.

v1.4.4

Choose a tag to compare

@github-actions github-actions released this 31 Oct 22:37

[1.4.4] - 2026-11-01

What changed

  • swagger.yaml (the bundled upstream API reference tracked in this repo) was synced from NPG 2.65.3 to 2.65.4. The file is now byte-identical to the upstream v2.65.4 tag (sha256 f2c72ff6…), so the spec shipped with the server no longer lags the API the tools are written against.
  • The POST /challenge/verify operation — the endpoint behind npg_verify_challenge — was corrected in the spec to the contract the tool has implemented since v1.4.3: request body required: [token] (the phantom solution property is gone), with proxy_host_id (uuid, "selects the config") and challenge_reason documented. The 200 response now documents that a rejected CAPTCHA comes back as success: false with error/score, and that a success carries the bypass token plus expires_at/expires_in; 400 (malformed body / bad proxy_host_id) and 503 (challenge not enabled or no CAPTCHA configured) are now described. Previously the spec advertised the wrong request shape, so anyone reading it instead of tool-schemas.yaml was steered into the exact bug fixed in v1.4.3.
  • Exploit-rule exclusion operations had their error contracts tightened in the spec: 400 now reads "Host or rule ID missing or not a valid ID" (matching the upstream UUID validation), and the per-host exclusion paths gained the 404 Proxy host not found response they already returned. The rule-not-found 404 wording was simplified to "Proxy host or rule not found".
  • File hygiene: the spec now ends with a trailing newline instead of \ No newline at end of file.
  • No server code changed in this release. Re-confirmed on the release commit: 308 tools with 0 duplicates, ruff check . clean, docs --check exit 0 (including the 460-char description cap, longest npg_bulk_import_proxy_hosts at 460), and tier counts read=139 / standard=261 / full=308 matching tier_allowed().

What's new

  • No new tools, parameters or capabilities — this is a reference-spec sync only. The tool surface stays at 308 tools, same names, same tiers (read=139 / standard=261 / full=308), and every tool behaves exactly as in 1.4.3.
  • The bundled swagger.yaml now documents the per-host CAPTCHA verification contract (proxy_host_id, challenge_reason, rejected-token success: false) that npg_verify_challenge actually implements, so spec readers and tool callers agree.

Breaking changes

  • (none) No tool, parameter, default, auth or config requirement changed.

v1.4.3

Choose a tag to compare

@github-actions github-actions released this 31 Oct 17:34

[1.4.3] - 2026-11-01

What changed

  • npg_verify_challenge sent a phantom solution field that does not exist in upstream model.VerifyCaptchaRequest ({token, proxy_host_id, challenge_reason}), and never sent proxy_host_id. The request therefore always resolved the global CAPTCHA config: every host that had its own CAPTCHA keys got HTTP 503 Challenge is not enabled even though its per-host challenge config was live. The body now mirrors the upstream model exactly — token (required), proxy_host_id, challenge_reason — and proxy_host_id is sent as a raw string (no percent-encoding) so the service's if req.ProxyHostID != "" branch selects the per-host config.
  • The same tool reported a rejected CAPTCHA as a successful call. Upstream answers a failed verification with HTTP 200 {"success": false, "error": "CAPTCHA verification failed", ...}, and the tool used to wrap that in {"success": true, "data": {...}}. It now returns an envelope success: false with the upstream message, keeping the full upstream payload under data.
  • npg_update_proxy_host_challenge was missing three fields of upstream ChallengeConfigRequest: secret_key, page_message and theme. Without secret_key no CAPTCHA could be configured through MCP at all (has_secret_key stayed false, so every verification returned 503 Challenge is not configured). All three are optional and None-defaulted, so the tool remains a partial update — omitted fields are never written.
  • Both description= strings were rewritten in the same commit as the signature changes, as the publish workflow's description-sync gate requires (npg_verify_challenge 407 chars, npg_update_proxy_host_challenge 431 chars, under the 460-char cap).
  • 12 new regression tests in tests/test_challenge_verify_config.py pin the verify request body (including "solution is never sent"), the client-side empty-token rejection, the upstream-success:false envelope mapping, the three new challenge-config fields, partial-update behaviour and the route. Full suite: 503 passed.
  • tool-schemas.yaml regenerated for the new parameters. This was needed because the docs drift gate compares tool names only, so a parameter-only change is invisible to --check and would otherwise have shipped stale parameter documentation.
  • Verified on the isolated test stack (NPG 2.65.4) against a HEAD-pinned local image local/npg-mcp:452f340@sha256:7a2a26fb… — not the stale published image. 21/21 checks passed on a throwaway host with its own Turnstile challenge config: schema checks (3 verify params, no solution), secret_key persisted (has_secret_key: true), page_message/theme round-tripped, a theme-only call left challenge_type/has_secret_key/page_message untouched, verify without proxy_host_id → 503 Challenge is not enabled (global config, as expected), verify with proxy_host_id → HTTP 200 plus a 64-char bypass token (expires_in: 86400), a rejected CAPTCHA → envelope success: false with data.error, and an empty token → client-side rejection with no API call. The throwaway host was deleted and confirmed gone afterwards.

What's new

  • npg_verify_challenge can now verify a CAPTCHA against a per-host challenge config via proxy_host_id (previously impossible from MCP), and can tag the verification with challenge_reason (bot_filter / geo_restriction), which upstream records on the issued bypass token and in the challenge log.
  • npg_update_proxy_host_challenge can now fully configure a CAPTCHA: secret_key (the missing piece that made verification impossible), plus the challenge-page page_message and theme options.
  • No new tools: the surface stays at 308 tools, same names, same tiers (read=139 / standard=261 / full=308).

Breaking changes

  • npg_verify_challenge no longer accepts solution — drop it from any call and pass the CAPTCHA provider response as token. The parameter never reached the API (upstream has no such field), so removals only fix validation errors; verification behaviour improves. Callers that relied on {"success": true} for a rejected CAPTCHA must now check success: false (and may read data.error).

v1.4.2

Choose a tag to compare

@github-actions github-actions released this 31 Oct 12:17

[1.4.2] - 2026-10-31

What changed

  • npg_update_proxy_host's description= advertised SSL toggling and stream configuration, but its signature could not accept them: ssl_enabled, proxy_type and the eight stream_* params (stream_listen_host, stream_listen_port, stream_protocol, stream_ssl_preread, stream_accept_proxy_protocol, stream_send_proxy_protocol, stream_proxy_connect_timeout, stream_proxy_timeout) were unreachable. Any agent following the published description hit a Pydantic validation error (npg_update_proxy_host() got an unexpected keyword argument), and there was no other way to switch an existing host to stream proxy mode or toggle its SSL through the MCP surface. All 10 are now real parameters wired into the existing param→API field map (50 params served, up from 40).
  • The same ssl_enabled gap was closed on npg_update_proxy_host_simple (9 params served), so the simple/advanced pair stays consistent.
  • The description= of all three changed tools was rewritten in the same commit as the signatures, as the publish workflow's description-sync gate requires: npg_update_proxy_host now names the stream surface explicitly, npg_update_proxy_host_simple lists ssl_enabled, and npg_update_cloud_provider states the upstream update contract (region and slug are create-only; enabled is a real update field) instead of implying every creation field is updatable.
  • npg_mcp/upstream_models.py now records why the npg_update_cloud_provider kwargs whitelist is correct: PUT /cloud-providers/{slug} binds upstream model.UpdateCloudProviderRequest, not CreateCloudProvider — the swagger $ref is stale. The static preflight scan had twice reported enabled there as a phantom field; the comment stops that false positive from recurring.
  • 11 new regression tests in tests/test_proxy_host_update_ssl_stream.py pin the reachable parameter sets, the param→body mapping (including ssl_forced→ssl_force_https), the partial-update no-op behaviour, and description↔signature agreement. Full suite: 491 passed.
  • tool-schemas.yaml regenerated. It was stale for all three tools — the docs drift gate compares tool names only, so a parameter-only change is invisible to --check and would otherwise have shipped parameter documentation missing 10 params.
  • Verified on the isolated test stack (NPG on 192.168.0.140) against a HEAD-pinned local image local/npg-mcp:a7b0fd7@sha256:7c5266ef…, not the stale published image — target identity proven by comparing the container's installed main.py sha256 with the workspace file at HEAD. 21/21 fix-claim checks passed on fresh verify- fixtures: ssl_enabled toggled false→true→false with each step confirmed by an independent GET, all nine proxy_type/stream_* values round-tripped exactly (mismatch={}), a host_id-only body remained a strict no-op (unrelated fields survived), wrong-typed arguments were rejected at the schema layer, and npg_update_cloud_provider flipped enabled false→true while region stayed unchanged after client-side rejection of region/slug. Gates: 308 tools, 0 duplicates, ruff + mypy clean, pytest 491 passed, docs --check exit 0 including the 460-char description cap, tier counts read=139 / standard=261 / full=308, 5 unrelated read-only tools called without regression. All fixtures and the throwaway container were deleted afterwards.
  • Verification also surfaced three upstream-semantics observations that are not MCP-layer bugs and are not fixed here: switching proxy_type back from stream to http additionally requires forward_scheme="http" in the same call (upstream rewrites the stored scheme to tcp for stream hosts), stream_listen_port range validation is lazy (an http host stores an out-of-range port silently; only a stream host rejects it), and npg_list_cloud_providers reports paging: "unexpected upstream shape" when upstream returns JSON null for an empty collection (cosmetic, pre-existing).

What's new

  • npg_update_proxy_host can now switch an existing host between HTTP and stream proxy modes (proxy_type) and configure stream listeners (stream_listen_host, stream_listen_port, stream_protocol, stream_ssl_preread, stream_accept_proxy_protocol, stream_send_proxy_protocol, stream_proxy_connect_timeout, stream_proxy_timeout) — configuration that previously required hand-crafted API requests.
  • npg_update_proxy_host and npg_update_proxy_host_simple can now toggle ssl_enabled on an existing host, matching what both descriptions had already promised.
  • No new tools: the surface stays at 308 tools, same names, same tiers (read=139 / standard=261 / full=308).

Breaking changes

  • (none) No tool, parameter, or default was renamed or removed. Every added parameter is optional and None-defaulted, so omitted fields are still left unchanged — existing calls behave identically.

v1.4.1

Choose a tag to compare

@github-actions github-actions released this 30 Oct 15:34

[1.4.1] - 2026-10-31

What changed

  • npg_create_sso_provider's signature now accepts the five parameters its own description= had advertised: allowed_email_domains, allowed_emails, group_claim, required_group, default_role_id. All five are optional and are written to the POST /api/v1/sso-providers body only when provided (conditional body building, so omitted fields stay omitted). Any agent following the published description previously hit a Pydantic validation error — npg_create_sso_provider() got an unexpected keyword argument.
  • Measured impact of the pre-fix behaviour while verifying: the previously published image served a 7-property schema (0 of 5 advertised params) and, on a call passing all five, returned success: true while silently dropping 4 of 4 security fields (allowed_email_domains, allowed_emails, required_group, default_role_id came back as [] / "" / null). This was silent data loss on an access-control surface, not just a cosmetically stale description. The fixed build serves all 5 (12 properties) and a live create persists every value exactly, confirmed by an independent npg_list_sso_providers read-back.
  • The tool's description= was rewritten in the same commit as the signature change (431 chars) and now states the auth gate explicitly: (email OR domain) AND required_group; no allowlist means nobody can sign in. It also notes client_secret is required for most IdPs and that default_role_id must be an existing role UUID.
  • 8 new regression tests in tests/test_sso_provider_create.py pin the parameter set, the conditional body mapping, and description↔signature agreement. Full suite: 480 passed.
  • tool-schemas.yaml regenerated in a follow-up commit (f84c2c0) for the new parameters. This was needed because the docs drift gate compares tool-schemas.yaml by tool name set only, so a parameter-only change is invisible to --check and would otherwise ship stale parameter documentation silently.
  • Committed swagger.yaml synced to upstream NPG v2.65.3 (3-line diff; no route or request-struct change, so no MCP surface change beyond the fix above).
  • Verified on the isolated test stack against a HEAD-pinned local image (local/npg-mcp:f84c2c0), not the stale published image: target identity proven by comparing the container's installed main.py sha256 with the workspace file at HEAD; 12/12 checks pass, edge cases clean (empty arguments, empty lists, 64-char name cap rejection, unicode / special characters / IDN values, wrong-type arguments, missing optional params). Gates: 308 tools, 0 duplicate names, docs --check exit 0 including the 460-char description cap, tier counts read=139 / standard=261 / full=308, 5 unrelated tools called without regression.
  • Verification also surfaced an upstream constraint that is not an MCP-layer bug and is not fixed here: group_claim longer than 64 characters makes both POST and PUT /api/v1/sso-providers return HTTP 500 {"error":"Operation failed"} although swagger declares no maxLength (64 chars works, required_group accepts 250). Reproduced with raw HTTP and the MCP layer bypassed, so the tools pass the value through faithfully; failure is atomic (no partial row). Filed as an upstream suspicion for the pipeline to verify and report.

What's new

  • npg_create_sso_provider can now create SSO providers that actually restrict access by email domain (allowed_email_domains), by explicit email allowlist (allowed_emails), by a named OIDC group claim (group_claim, default groups), by a required IdP group (required_group), and can grant a specific existing role to auto-provisioned users (default_role_id) — all previously only reachable by hand-crafting the API request.
  • No new tools: the surface stays at 308 tools, same names, same tiers.

Breaking changes

  • (none) No tool, parameter, or default was renamed or removed; the five added parameters are optional, so existing calls behave identically.

v1.4.0

Choose a tag to compare

@github-actions github-actions released this 28 Oct 02:08

[1.4.0] - 2026-10-28

What changed

  • npg_mcp/toolsets.py gained a derived MCP ToolAnnotations layer: annotate_tools(mcp), called once from main() before the tier filter, attaches readOnlyHint / destructiveHint / idempotentHint / openWorldHint plus a derived title to every registered tool. Classification is AST-derived from the HTTP verbs each tool body calls in main.py (get/get_text/get_bytes = read; post/post_file/put/delete = mutating) combined with the existing DESTRUCTIVE_TOOLS naming registry — there is no hand-maintained 308-name table. Two documented exception sets: _READONLY_DENYLIST (GET routes with side effects — npg_check_backup_restore writes and deletes a throwaway backup row) and _CLOSED_WORLD_TOOLS (11 closed-catalog reads).
  • Annotations are attached by mutating the FastMCP Tool objects instead of editing decorator lines, so all 308 @mcp.tool(name=..., description=...) decorators stay byte-identical and the AST-driven doc tooling (regenerate_all_docs.py, check_tool_descriptions.py, the CI description-sync and DESC_LEN gates) keeps parsing them unchanged. Zero signature changes, zero description changes, zero tools added/removed/renamed — tier counts stay read=139 / standard=261 / full=308.
  • ToolAnnotations.model_validate({...camelCase...}) is used rather than kwargs construction: mcp 1.x declares the fields camelCase while mcp 2.x renames them snake_case with camelCase aliases, so alias validation is the only construction valid across the supported mcp>=1.29,<2 range.
  • tests/test_toolsets.py gained 15 tests pinning the derivation itself: every tool annotated, every tool body yields route data, destructive ⇒ destructiveHint, GET-only ⇒ readOnlyHint, mutating routes never read-only, idempotent rules, denylist/closed-world liveness, title derivation, hints reaching the served tools/list, plus idempotent and defensive annotate_tools behavior.
  • README.md / README.ko.md gained a "Tool Annotations" subsection; the toolsets.py Project Structure line now mentions annotations.
  • Verified on the isolated test stack against a HEAD-pinned local image (local/npg-mcp:576211b, label revision = 576211b): 21/21 live checks (308 tools served, all annotated, hint sets 141/47/185/11, spot semantics, derived titles) plus real tool calls — npg_get_settings, fixture npg_create_proxy_host → npg_delete_proxy_host → 404 confirm — and 7/7 read-tier checks on a second container at NPG_TOOL_LEVEL=read (139 tools, all annotated, destructive tools still unlisted and uncallable). Gates: pytest 472 passed, ruff + mypy clean, docs drift --check exit 0 (incl. DESC_LEN), --check-env tier counts match.

What's new

  • Every tool now carries standard MCP ToolAnnotations in tools/list: 141 read-only (readOnlyHint=true), 47 destructive (destructiveHint=true), 185 idempotent reads/PUT partial updates, and openWorldHint=false for the 11 closed-catalog reads. MCP clients (Hermes, Claude, Cursor) can now filter read-only tools, warn before destructive calls, and batch idempotent ones — with no change to tool execution.

Breaking changes

  • (none) Tool names, descriptions, parameter signatures, and toolset tiers are unchanged; annotations are additive client-facing metadata.