12 of 33 MCP tools now render as rich inline cards instead of raw JSON: EntityCard, HealthSummary, LXCList, EntityTable, KnowledgeResults, BlastRadius, ChangeLog, FleetSnapshot, MetricChart. Architecture: - Server: annotateJSONResult() wraps queryRows with __renderer hints - Registry: match/dispatch system maps tool names to Svelte components - Chat: inline dispatch with 5-card limit, overflow to collapsed group - ToolCallGroup: unmatched prop, hides when all matched, ARIA labels Tests: 3 new Go tests for annotateJSONResult (wrap, no-op, multi-row).
67 lines
3.1 KiB
Markdown
67 lines
3.1 KiB
Markdown
# 2026-07-13 — MCP tool apps: custom in-chat renderers
|
|
|
|
**Status:** Done — implemented 2026-07-13.
|
|
|
|
## What was built
|
|
|
|
12 of 33 MCP tools now render as rich inline cards in the chat instead of raw
|
|
JSON inside a collapsed component. The remaining 21 tools stay collapsed.
|
|
|
|
### Architecture
|
|
|
|
- **Server** (`internal/mcp/server.go`): `annotateJSONResult()` function wraps
|
|
`queryRows` output with `{"__renderer": "...", "data": [...]}` for 12 tools.
|
|
- **Registry** (`web/src/lib/tool-renderers.ts`): match/dispatch system that
|
|
maps tool names + `__renderer` hints to Svelte components.
|
|
- **Renderer components** (`web/src/lib/renderers/`): 9 purpose-built cards,
|
|
each handling loading/spinner, error, and success states with proper ARIA
|
|
labels.
|
|
- **Chat dispatch** (`web/src/pages/Chat.svelte`): matched tools render inline
|
|
before the markdown text, with a 5-card limit to prevent chat spam. Overflow
|
|
goes to the collapsed `ToolCallGroup` alongside unmatched tools.
|
|
- **ToolCallGroup** (`web/src/lib/components/ToolCallGroup.svelte`): accepts
|
|
`unmatched` prop, shows "N tools · M cards shown" when some render inline,
|
|
hides entirely when all matched.
|
|
|
|
### Renderers
|
|
|
|
| Component | Tools matched | Visual |
|
|
|-----------|--------------|--------|
|
|
| `EntityCard` | `get_entity`, `whoami`, `explain` | Slug, type badge, health dot, key attrs |
|
|
| `HealthSummary` | `get_health_summary` | Stacked health bar (healthy/degraded/down) |
|
|
| `LXCList` | `list_lxcs` | Compact table: name, ID, IP, health |
|
|
| `EntityTable` | `list_entities` | Auto-column table from query results |
|
|
| `KnowledgeResults` | `search_knowledge`, `get_entity_knowledge` | Title, snippet, source, slug |
|
|
| `BlastRadius` | `get_blast_radius` | Entities grouped by hop distance |
|
|
| `ChangeLog` | `get_change_history`, `get_agent_activity` | Timeline with status dots |
|
|
| `FleetSnapshot` | `get_state_snapshot` | Health + type counts in a grid |
|
|
| `MetricChart` | `query_metrics` | Bucketed time/avg/min/max table |
|
|
|
|
### Tests
|
|
|
|
`internal/mcp/server_test.go`: 3 new tests for `annotateJSONResult` — wraps
|
|
valid JSON arrays, no-op on empty/non-JSON/empty-text content, preserves
|
|
multi-row arrays.
|
|
|
|
### Files changed
|
|
|
|
**New (18):**
|
|
- `web/src/lib/tool-renderers.ts`
|
|
- `web/src/lib/renderers/index.ts`
|
|
- `web/src/lib/renderers/EntityCard.svelte` + `entity-card.ts`
|
|
- `web/src/lib/renderers/HealthSummary.svelte` + `health-summary.ts`
|
|
- `web/src/lib/renderers/LXCList.svelte` + `lxc-list.ts`
|
|
- `web/src/lib/renderers/EntityTable.svelte` + `entity-table.ts`
|
|
- `web/src/lib/renderers/KnowledgeResults.svelte` + `knowledge-results.ts`
|
|
- `web/src/lib/renderers/BlastRadius.svelte` + `blast-radius.ts`
|
|
- `web/src/lib/renderers/ChangeLog.svelte` + `change-log.ts`
|
|
- `web/src/lib/renderers/FleetSnapshot.svelte` + `fleet-snapshot.ts`
|
|
- `web/src/lib/renderers/MetricChart.svelte` + `metric-chart.ts`
|
|
|
|
**Modified (5):**
|
|
- `internal/mcp/server.go` — `annotateJSONResult()` + 12 tool annotations
|
|
- `internal/mcp/server_test.go` — 3 tests for `annotateJSONResult`
|
|
- `web/src/lib/components/ToolCallGroup.svelte` — `unmatched`/`bodyTools`
|
|
- `web/src/pages/Chat.svelte` — inline dispatch + 5-card limit
|
|
- `web/src/main.ts` — deferred renderer import
|