Skip to content

Changelog

[1.0.0] — 2026-04-28

Added

  • display_hint on every tool response — top-level object with a required frame string and optional primary_key / title fields. Tells downstream renderers which visual frame to use for each tool by default, removing the need to infer layout from JSON shape heuristics. The frame for each tool is fixed by the binding table in the spec (see SYSTEM_PROMPT.md and docs/DEVELOPMENT.md):
Tool frame
netbox_search hits-by-type
netbox_get table
netbox_get_all table
netbox_inspect_type raw
netbox_query raw
netbox_aggregate_by_device bar-chart
netbox_count_devices_by tree-or-bar
netbox_device_profile card-grid
netbox_find_ip_references trio
netbox_trace_path timeline-or-graph
netbox_get_available table

primary_key is included where a sensible row identifier exists ("id" for table frames, "device.id" for card-grid and bar-chart frames, "parent.id" for tree-or-bar). title is derived from tool arguments where meaningful (e.g. "Top devices by tunnel_terminations" for netbox_aggregate_by_device, "Devices grouped by dcim.region" for netbox_count_devices_by).

The display_hint is injected via result.setdefault(...) — if a service already returns a display_hint, the service value is preserved (forward-compat precedence rule, consistent with the tool_name / elapsed_ms pattern).

The frame vocabulary (FRAME_VOCABULARY: frozenset[str]) is defined as a single module-level constant in src/netbox_agent_mcp/tools/_display_hint.py and is imported by contract tests to assert the vocabulary equals the binding set exactly.

Changed — BREAKING

  • netbox_trace_path and netbox_get_available return type changed from list to dict[str, Any]. The returned list is now nested under a "path" key (netbox_trace_path) or a "results" key (netbox_get_available), so the response carries the same top-level envelope (tool_name, elapsed_ms, display_hint) as every other tool. This is the only breaking change in the release.

Migration:

# before
hops = netbox_trace_path(device="r1", interface="ge-0/0/0")
for hop in hops:
    ...

free = netbox_get_available(object_type="ipam.prefix", parent_id=42)
for prefix in free:
    ...
# after
result = netbox_trace_path(device="r1", interface="ge-0/0/0")
for hop in result["path"]:
    ...

result = netbox_get_available(object_type="ipam.prefix", parent_id=42)
for prefix in result["results"]:
    ...

The shape of every other tool's response is unchanged for fields that already existed; the tool_name / elapsed_ms / display_hint additions are additive.

Because this changes the return type of two public tools, the next release is a SemVer MAJOR bump (1.0.0).

[0.2.0] — 2026-04-28

Added

  • tool_name on every tool response — top-level string field echoing the called tool's __name__ (e.g. "netbox_get"). Lets renderers, audit drawers, and right-rail cards identify which tool produced a response without parsing the MCP call path or relying on bridge metadata.
  • elapsed_ms on every tool response — top-level integer field recording server-side wall-clock execution time in milliseconds (measured via time.perf_counter_ns to avoid CPU-time skew). More accurate than browser-measured fetch latency, which includes mcpo bridge overhead and network round-trip. Intended for timing strips and per-call latency analysis in Phase 4's right-rail card.

Both fields are injected by a new with_response_envelope decorator applied at tool registration time in server.py. The decorator is transparent: it uses functools.wraps to preserve the original signature and docstring that FastMCP introspects for the MCP schema, never touches tool internal logic, and honours tool-provided values — if a tool already returns tool_name or elapsed_ms in its dict, those values are preserved unchanged. These are additive, optional fields; no existing response keys are modified.

[0.1.10] — 2026-04-23

Fixed

  • Scrubbed a multicast CIDR that had been copied verbatim from a user debug trace into tests/unit/schema/test_ip_scope.py. Replaced with a generic TEST-NET-equivalent (239.1.2.0/24). Git history was rewritten (force-push) to remove the value from every commit + commit message; the v0.1.9 tag was re-pointed at the clean SHA; the Actions artifacts containing the leaked sdist were deleted. 0.1.9 should be treated as recalled — the PyPI sdist contains the leaked test fixture and should be yanked / deleted by the maintainer.

[0.1.9] — 2026-04-23 (YANKED)

Added

  • ip_scope filter key on netbox_get / netbox_get_all — post-filters ipam.ipaddress / ipam.prefix / ipam.iprange results by address classification using Python's ipaddress stdlib. Values: public, private (RFC 1918 + CGNAT + IPv6 ULA), documentation (RFC 5737 + RFC 3849), loopback, link-local, multicast, reserved. Response includes an ip_scope_filter field with the before/after counts; total_count reflects the pre-filter NetBox count so callers can detect the delta. Invalid scope values and non-IP object types raise ValueError eagerly. New schema/ip_scope.py module + 12 tests covering every scope class (IPv4 + IPv6) and the dropped-for-narrow-scope-junk-records edge case.
  • scope_under_site_id filter key on netbox_get for ipam.prefix / ipam.vlan — closes the NetBox 4.x polymorphic-scope gap where site_id=N on a prefix only matches records whose scope_type == "dcim.site" and silently misses records scoped to Locations nested under that site. The magic key resolves under the hood: fetches the site's locations, then unions direct-site-scoped and location-scoped queries and deduplicates. Response includes a scope_resolution block with per-sub-query match counts for transparency. Not supported on netbox_get_all (each sub-query's total is independently capped; use netbox_get with paging instead). 7 tests covering the union, dedup, empty-locations fast path, and rejection on non-scoped types.

Fixed

  • netbox_get / netbox_get_all docstrings now document both magic filter keys alongside the existing "q" / __in / __ic forms. SYSTEM_PROMPT.md intent table adds explicit rows for "public/private IPs" and "everything scoped under site X" so the agent routes to these filters instead of hand-rolling subnet math or writing off location-scoped prefixes as missing.

[0.1.8] — 2026-04-23

Added

  • New tool netbox_count_devices_by(parent_type, top_n=0) — answers "device counts broken down by region / site-group / site / location / role / tenant" in a single complete call, eliminating the small-model failure mode of netbox_get("dcim.site", limit=100) + truncated client-side aggregation on larger deployments. Picks the right strategy per parent type: dcim.site/dcim.location/dcim.devicerole/tenancy.tenant use NetBox's pre-computed device_count annotation (direct scan); dcim.region and dcim.sitegroup sum site.device_count across every site via a full paginated scan. For region/sitegroup the response includes an unassigned_devices bucket (devices in sites with no region/group) so totals reconcile against the global device count.
  • DeviceCountByParentService — new service + 7 tests covering the real-world failure scale (403+ sites, full pagination, region/sitegroup rollup, direct-count path, top-N semantics preserving totals, unassigned handling).

Fixed

  • Removed the misleading "order by -device_count" breadcrumb for regions/sitegroups. The SYSTEM_PROMPT.md intent row for "Top N parents by pre-computed count" now explicitly scopes to the types that actually carry the annotation (dcim.site, dcim.location, dcim.devicerole, tenancy.tenant) and routes region/sitegroup to the new tool. When an agent asks ordering="-device_count" on dcim.region or dcim.sitegroup, the fields_dropped_hint now explicitly tells them to use netbox_count_devices_by instead of silently dropping the field.

[0.1.7] — 2026-04-23

Added

  • Region and site-group traversal now surfaced to agents. NetBox's region_id filter on dcim.device / dcim.rack / circuits.circuit / ipam.prefix / ipam.vlan / virtualization.virtualmachine walks {object}.site.region natively and automatically includes descendant regions in the tree — but nothing in the server was telling the agent this. After a region search, the agent previously got only sites_in_these_regions and had to chain region → sites → devices in three calls. Now compute_suggested_filters emits the full traversal suite (devices_in_these_regions, racks_in_these_regions, circuits_in_these_regions, prefixes_in_these_regions, vlans_in_these_regions, vms_in_these_regions), and dcim.region carries a type_guidance hint explaining the pattern.
  • Same treatment for dcim.sitegroup (functional grouping that cross-cuts geography): new devices_in_these_sitegroups / racks_in_these_sitegroups via the site_group_id traversal filter, plus guidance.
  • SYSTEM_PROMPT.md intent table adds a dedicated row for "devices / racks / circuits / prefixes / VLANs / VMs in region X" showing the one-call pattern.

[0.1.6] — 2026-04-23

Fixed

  • netbox_find_ip_references now surfaces NetBox 4.x's polymorphic scope on containing prefixes and IP ranges. Previously the tool's _distill_container pulled only the legacy site FK, which NetBox 4.x populates as a backward-compat virtual field only when scope_type == "dcim.site". When a prefix was scoped to a Location, Region, or SiteGroup (e.g. a building floor, a metro region), site was null and the agent reported "no site associated" even though the real location was right there in scope. The distilled container now exposes both scope_type + scope (preferred) and site (legacy/backward-compat), and the summary.hint labels the scope level ("location X" vs. "site Y") so agents don't conflate hierarchy tiers.
  • netbox_find_ip_references docstring now calls out scope-vs-site semantics explicitly, and docs/SYSTEM_PROMPT.md instructs agents to read scope first and fall back to site only when scope is null (pre-4.x NetBox).

[0.1.5] — 2026-04-23

Docs (agent-facing)

  • netbox_get filter grammar now documents the "q" free-text key. Example: netbox_get("dcim.site", {"q": "london"}, select=[...], ordering="-device_count") — same ?q= semantics as netbox_search, scoped to one type, with agent-chosen field projection and order-by. This is the efficient idiom for "find all [sites / devices / VLANs / circuits / racks] matching X" when the type is known.
  • netbox_search docstring reworked to explicitly steer agents toward netbox_get when the type is known and richer columns are needed, and to keep netbox_search for type-unknown discovery and for responses where suggested_filters / per-type truncation signals matter.
  • SYSTEM_PROMPT.md intent table updated with a dedicated row for scoped searches by type.

No code changes — this is pure prompt engineering. Agents running 0.1.4 with the old prompt already had this capability; 0.1.5 just makes them discover it reliably.

[0.1.4] — 2026-04-23

Added

  • netbox_device_profile now accepts a fields=[...] projection list that slims the top-level device record and every item in every section down to just the requested fields (id + display are always kept as navigation anchors). Supports dotted paths like "site.name" for nested projection. Combined with batched device_ids + max_per_category, agents can now get N devices' worth of data with surgical token cost — e.g. for a "devices at site X" summary table, fields=["name","role","status","primary_ip4.address"] drops payload size by ~10x vs. the default brief shape.
  • Docstring + docs/SYSTEM_PROMPT.md updated to instruct agents to always pass fields alongside device_ids for batched calls.

[0.1.3] — 2026-04-23

Added

  • netbox_device_profile now accepts a list of device IDs for batched, parallel profiling. Call netbox_device_profile(device_ids=[D1, D2, ...]) once instead of N sequential device_id=Di calls. Up to 50 devices per batch, fanned out across 5 parallel workers, with per-device failure isolation (one bad device doesn't abort the batch — it gets an error field in the response while the rest return normally). Response shape: {profiles: [...], meta: {requested, returned, failed}}; profile order preserves caller's device_ids order.
  • Docstring + docs/SYSTEM_PROMPT.md updated to instruct agents to prefer the batch form whenever a list of devices is on hand (e.g., right after netbox_get('dcim.device', ...) or netbox_aggregate_by_device).

[0.1.2] — 2026-04-23

Fixed

  • Replaced real-world IP addresses, site codes, and device hostnames that had been copied verbatim from a user-provided debug trace into the 0.1.1 docstring + tests. All now use RFC 5737 documentation addresses (192.0.2.x, 198.51.100.x, 203.0.113.x) and generic names (site-a, site-b, router-edge-a). 0.1.1 should be treated as recalled — it contains placeholder but non-RFC-5737 data that resembles real infrastructure and should not be distributed.

[0.1.1] — 2026-04-23 (YANKED)

Fixed

  • netbox_find_ip_references now locates addresses inside tracked prefixes and IP ranges. Previously the tool only checked per-host ipam.ipaddress records and free-text descriptions; agents reported "IP not found" for addresses that lived inside a known subnet (the common case in deployments that track prefixes densely but host records sparsely). The tool now also queries ipam/prefixes?contains=<ip> and ipam/ip-ranges?contains=<ip>, returns the enclosing records under containing_prefixes / containing_ip_ranges with site + role + description, and updates summary.hint to steer the agent at the containing record's site when there's no per-host match.
  • Updated tool docstring and docs/SYSTEM_PROMPT.md to call out the multi-layer lookup and explicitly forbid "IP not found" answers before containing_prefixes has been read.

Fixed

  • netbox_find_ip_references now locates addresses inside tracked prefixes and IP ranges. Previously the tool only checked per-host ipam.ipaddress records and free-text descriptions; agents reported "IP not found" for addresses that lived inside a known subnet (the common case in deployments that track prefixes densely but host records sparsely). The tool now also queries ipam/prefixes?contains=<ip> and ipam/ip-ranges?contains=<ip>, returns the enclosing records under containing_prefixes / containing_ip_ranges with site + role + description, and updates summary.hint to steer the agent at the containing record's site when there's no per-host match.
  • Updated tool docstring and docs/SYSTEM_PROMPT.md to call out the multi-layer lookup and explicitly forbid "IP not found" answers before containing_prefixes has been read.

Docs

  • MkDocs Material site auto-built + deployed to GitHub Pages at https://magicboxlab-ai.github.io/netbox-agent-mcp/. Adds pyproject.toml [dependency-groups].docs, mkdocs.yml, a landing page + thin wrappers for root-level docs, and .github/workflows/pages.yml. README now carries PyPI + Docs badges.

[0.1.0] — 2026-04-22

First public release. Agentic MCP server for NetBox with ten read-only tools: netbox_search, netbox_get, netbox_get_all, netbox_aggregate_by_device, netbox_device_profile, netbox_find_ip_references, netbox_trace_path, netbox_get_available, netbox_inspect_type, netbox_query (raw GraphQL).

Docs

  • Repo-level AI-assistant guide (git-ignored, personal-to-maintainer).
  • docs/DEVELOPMENT.md consolidates design, implementation, testing, and extending in one developer guide.

Repository hygiene

  • Scrubbed realistic-looking hostnames and IPs from tests + one docstring (RFC 5737 addresses + generic router-a / router-b placeholders).
  • .pre-commit-config.yaml (gitleaks + ruff + standard hooks) and .gitleaks.toml with an allowlist for synthetic nbt_* test tokens.
  • SECURITY.md with a private vulnerability-reporting policy.
  • CONTRIBUTING.md and CODE_OF_CONDUCT.md (Contributor Covenant v2.1 by reference).
  • .github/workflows/ci.yml — ruff + pytest (3.11 / 3.12 / 3.13) + gitleaks on push + PR.
  • .github/workflows/publish.yml — tag-triggered PyPI publish via trusted publishing (OIDC, no tokens).
  • .github/dependabot.yml + dependabot-auto-merge.yml — weekly uv + github-actions updates, patch/minor auto-merged on green CI, majors flagged for manual review.
  • Issue templates (bug / feature) and PR template.
  • pyproject.toml authors, urls, keywords, and PyPI classifiers.
  • README badges: CI, Python versions, license, pre-commit, Ruff.
  • README install path leads with uvx and uv tool install; git clone moved to a Development install section.

Code quality

  • __in suffix-stripping filter translation centralized in schema/filter_builder.filters_to_rest_params; QueryService and DeviceAggregationService share it.
  • Removed unreachable last_exc / assert dead code from RestClient._request and GraphQLClient.query.
  • server.main closes the shared httpx.Client on shutdown; build_services returns (registry, http).
  • Deleted unreferenced GraphQL-first helpers: schema/select_compiler.py and the GraphQL portion of schema/filter_builder.py (SUFFIX_MAP, build_filter_args, support fns).