Skip to content

feat(mcp): device_view MCP App (KLA-404)#29

Merged
jklaassenjc merged 3 commits into
mainfrom
juergen/kla-404-device-view
May 20, 2026
Merged

feat(mcp): device_view MCP App (KLA-404)#29
jklaassenjc merged 3 commits into
mainfrom
juergen/kla-404-device-view

Conversation

@jklaassenjc
Copy link
Copy Markdown
Collaborator

@jklaassenjc jklaassenjc commented May 20, 2026

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

  • New tool device_view with input device (hostname, displayName, or 24-char hex ID). Resolves via resolve.DeviceConfig.
  • New ui:// resource ui://jc/device serving apps_html/device.html.
  • One s.registerDeviceView() call added in registerAppTools().

Backend (internal/mcp/apps_device.go)

  • Fetches the device synchronously (/api/systems/{id}), then fans out four best-effort goroutines:
    1. Groups: V2 /systems/{id}/memberof
    2. Applied policies: V2 /systems/{id}/associations?targets=policy + a single /policies fetch joined client-side for names (instead of N+1 GETs)
    3. System insights snapshot: V2 /systeminsights/{uptime|logged_in_users|disk_info} with system_id:eq:<id> filter, row-limited to 10
    4. Recent Directory Insights events: 30-day window, SearchTermFilter: {"system.id": id}, capped at 50
  • Per-fetch failures land in data.Warnings; only an unfetchable header surfaces as a tool error. Same contract as user_view.
  • connectivityBucket mirrors the dashboard's logic (<1h online, <24h recent, <7d stale, else offline).

UI (apps_html/device.html)

  • Header: OS-aware avatar (), hostname/OS/serial/agent/last-contact meta, 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 (apps_device_test.go)

  • Unit: connectivityBucket covers every threshold incl. invalid input
  • Aggregator: full happy-path fixture covers 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 carries _meta.ui.resourceUri; the ui:// resource serves the HTML with common.js injected
  • Updated TestMCP_ListTools_AllRegistered tool count (197 → 198) and expected list

Out of scope (deliberate, follow-up)

  • Installed apps list. Requires OS-branching across systeminsights tables (programs Windows, apps macOS, deb_packages/rpm_packages Linux) — 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.id search-term filter. From Claude Desktop:

"jc": {
  "command": "/path/to/jc",
  "args": ["mcp", "serve"]
}

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.id event 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 ./... clean
  • gofmt -l clean on touched files
  • Local build smoke: make build && ./jc mcp tools | grep device_view finds the tool
  • Manual: render device_view from 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 a ui://jc/device resource 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.

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>
Comment thread internal/mcp/apps_device.go Outdated
`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>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit e4f9ca4. Configure here.

Comment thread internal/mcp/apps_html/device.html Outdated
Comment thread internal/mcp/apps_html/device.html Outdated
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>
@jklaassenjc jklaassenjc merged commit cefd9c4 into main May 20, 2026
7 of 8 checks passed
@jklaassenjc jklaassenjc deleted the juergen/kla-404-device-view branch May 20, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3 participants