# 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