feat(mcp): device_view MCP App (KLA-404)#29
Merged
Conversation
Symmetric to KLA-403 user_view: a rich inventory view for one device
that AI hosts can render in their UI. Same pattern, same conventions.
Backend (internal/mcp/apps_device.go):
- Tool: device_view. Input: device (hostname, displayName, or 24-char
hex ID). Resolves via resolve.DeviceConfig.
- Fetches the device record synchronously, then fans out four
best-effort goroutines: group memberships (V2 /systems/{id}/memberof),
applied policies with name resolution via the V2 policy catalog,
system-insights snapshot (uptime + logged_in_users + disk_info via
V2 /systeminsights/<table>?system_id), and recent Directory Insights
events filtered to the device.
- Per-fetch failures land in data.Warnings; only an unfetchable
header surfaces as a tool error. Matches the user_view contract.
- connectivityBucket mirrors apps.go's dashboard logic (<1h online,
<24h recent, <7d stale, else offline).
UI (internal/mcp/apps_html/device.html):
- Header with OS-aware avatar, hostname/OS/serial/agent/last-contact
meta, and status badges (connectivity, FDE, MDM, inactive).
- Details kv block, Groups pill list, Applied Policies table.
- System Insights card: uptime, current sessions, per-disk usage bars
(yellow at 75 %, red at 90 %).
- Recent events table (last 30 d).
- Refresh button reuses jcApp.callTool with the resolved device ID.
Tests (internal/mcp/apps_device_test.go):
- Unit: connectivityBucket buckets every threshold incl. invalid input.
- Aggregator: full happy-path fixture exercising header, status flags,
group sort, policy name lookup with missing-name fallback ordering,
systeminsights snapshot, and Directory Insights events.
- Smoke: a stale-bucket device produces Connectivity = "stale".
- Registration: device_view tool carries _meta.ui.resourceUri; the
ui:// resource serves the HTML with common.js injected.
Updated tool count in TestMCP_ListTools_AllRegistered (197 -> 198) and
added device_view to the expected-tools list.
Out of scope (deliberate, follow-up):
- Installed apps list. Requires OS-branching across systeminsights
programs (Windows), apps (macOS), deb_packages / rpm_packages
(Linux). Big query, big payload, worth its own ticket so the
current view ships small.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`FreeBytes int64 \`json:"free_bytes,omitempty"\`` was wrong for the
100 %-full disk case: an exact 0 was omitted from the payload, the
iframe's `typeof d.free_bytes === "number"` guard then failed, and
the usage bar rendered at 0 % instead of 100 % — exactly the case
we'd want the red "critical" bar. Free=0 is semantically meaningful
("full"), not "missing data."
Fix: remove `omitempty` from FreeBytes. SizeBytes stays optional
because size=0 really does mean "metadata unavailable" — both
behaviors converge to a no-usage-bar render, so keeping it optional
saves a few bytes in the rare "agent doesn't expose size" case.
New TestDeviceInsightsDisk_FullDiskKeepsFreeBytesField pins the
contract: a full disk must serialize `"free_bytes":0`, and a
no-size disk must still omit `size_bytes`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Reviewed by Cursor Bugbot for commit e4f9ca4. Configure here.
Two HTML-only follow-ups Bugbot caught after the FreeBytes omitempty fix in e4f9ca4. 1. fmtBytes treated `n <= 0` as "missing data," so a disk with FreeBytes=0 (full) rendered "—" in the Free column even though the Go side deliberately keeps the field present. Split the guards: negative / non-numeric still falls back to "—" (signals "agent didn't report"), but exact 0 renders as "0 B" (signals "100% full"). SizeBytes uses omitempty on the Go side so missing-size still arrives as undefined and shows "—". 2. osInitials returned an empty string for mac / darwin. Every other branch returns visible 2-char text (WN, LX), so the macOS avatar circle rendered blank. Set it to "MC" to stay consistent with the existing convention; deliberately not the Apple logo glyph because it lives in a private-use Unicode block and ships blank in many iframe-sandbox fonts. HTML-only change — Go tests unaffected, full suite still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jrennichjc
approved these changes
May 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes KLA-404. Symmetric to KLA-403 user_view — adds a rich device inventory view that MCP App-capable hosts (Claude Desktop, basic-host, etc.) render inline.
Summary
device_viewwith inputdevice(hostname, displayName, or 24-char hex ID). Resolves viaresolve.DeviceConfig.ui://jc/deviceservingapps_html/device.html.s.registerDeviceView()call added inregisterAppTools().Backend (
internal/mcp/apps_device.go)/api/systems/{id}), then fans out four best-effort goroutines:/systems/{id}/memberof/systems/{id}/associations?targets=policy+ a single/policiesfetch joined client-side for names (instead of N+1 GETs)/systeminsights/{uptime|logged_in_users|disk_info}withsystem_id:eq:<id>filter, row-limited to 10SearchTermFilter: {"system.id": id}, capped at 50data.Warnings; only an unfetchable header surfaces as a tool error. Same contract as user_view.connectivityBucketmirrors the dashboard's logic (<1h online, <24h recent, <7d stale, else offline).UI (
apps_html/device.html)jcApp.callToolwith the resolved device IDTests (
apps_device_test.go)connectivityBucketcovers every threshold incl. invalid inputConnectivity = "stale"device_viewcarries_meta.ui.resourceUri; the ui:// resource serves the HTML with common.js injectedTestMCP_ListTools_AllRegisteredtool count (197 → 198) and expected listOut of scope (deliberate, follow-up)
programsWindows,appsmacOS,deb_packages/rpm_packagesLinux) — big query, big payload, deserves its own ticket so the current view ships small.Manual verification (the reviewer should do this)
The HTTP-side is well-covered by mocks, but only a real device exercises the V2 systeminsights filter and the Insights
system.idsearch-term filter. From Claude Desktop:Then ask: "Use the jc MCP server to show me the device view for
<hostname>."Expect: an iframe with the header, status badges, details, groups, policies (with names), uptime + sessions + disk bars, and recent events. The
system.idevent filter is a best-known guess at JumpCloud's event schema; if the timeline is empty on a known-active device, the filter shape is the likely culprit and can be tuned in a small follow-up.Test plan
go test ./...clean (incl. new connectivity/aggregator/registration tests)go vet ./...cleangofmt -lclean on touched filesmake build && ./jc mcp tools | grep device_viewfinds the tooldevice_viewfrom Claude Desktop on a real device (see above)🤖 Generated with Claude Code
Note
Medium Risk
Adds a new MCP tool/resource that fans out multiple JumpCloud API calls (V1/V2/Insights) and aggregates them concurrently, which could introduce subtle API/query/ordering issues or increased load if assumptions about filters/limits are wrong.
Overview
Adds a new MCP App tool,
device_view, plus aui://jc/deviceresource that renders an interactive device inventory panel (details, connectivity/FDE/MDM status, group memberships, applied policies, system-insights snapshot, and recent Directory Insights events).Implements a best-effort data aggregator that resolves a device identifier, fetches core device details, then concurrently fetches groups, policy associations (with a single policy-catalog join for names), selected system-insights tables, and a 30-day capped event timeline; sub-call failures are surfaced as warnings.
Extends test coverage with dedicated fixtures for the new aggregator/UI metadata/resource injection and updates the tool registry expectations/tool count to include
device_view.Reviewed by Cursor Bugbot for commit c45e07b. Bugbot is set up for automated code reviews on this repo. Configure here.