feat(chat): MCP tool apps — custom inline renderers for 12 tools
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

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).
This commit is contained in:
2026-07-13 22:46:56 +02:00
parent 680575e2cf
commit 8b50753746
27 changed files with 1092 additions and 43 deletions

View File

@@ -0,0 +1,66 @@
# 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