docs(apps): specify the v2 Apps tab (#1265)

Adds the missing Apps tab to the v2 UX specification. Apps are the subset
of tools whose `_meta.ui.resourceUri` advertises a UI resource (per
`@modelcontextprotocol/ext-apps`). The screen mirrors the legacy v1 layout:
a sidebar of apps and a right pane that toggles between an input form
(before launch) and the embedded MCP App via `AppBridge`.

- v2_ux.md: add `[Apps]` to the navigation tab list and a new "Apps Screen"
  subsection with both states (input form / running app).
- v2_ux_features.md: full Apps Screen feature spec — ASCII diagrams,
  detection rule, presentational contract for `AppsScreen` / `AppRenderer`
  (imperative ref shape so the screen stays "dumb" and the wiring layer
  drives the bridge).
- v2_screenshots.md: link the two reference PNGs from the legacy v1 client.
- v2_ux_storybook.md: add `AppListItem` (2.3a), `AppDetailPanel` (2.5a),
  `AppRenderer` (2.5b), and `AppsScreen` (3.2a); update Build Order.

Implementation work tracked on the v2 board: #1260–#1264.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-04-29 17:22:59 -04:00 • committed by GitHub
commit bcb5b643ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 315 additions and 19 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

View file

@ -51,6 +51,18 @@ Tool list with annotations (user, read-only, destructive, long-running), paramet
![Tools Screen](screenshots/shadcn-tools.png)
### Apps Screen
The subset of tools that are MCP Apps — tools with `_meta.ui.resourceUri`. Two-panel layout: list of apps on the left, input form (before launch) or embedded app iframe (after launch) on the right.
**Input form state** (from legacy v1 reference):
![Apps Screen — Input Form](screenshots/apps-tab-form.png)
**Running app state** (from legacy v1 reference):
![Apps Screen — Running App](screenshots/apps-tab-app.png)
### Resources Screen
Accordion layout with Resources, Templates, and Subscriptions sections.

View file

@ -13,6 +13,7 @@
* [Server Info (Connected)](#server-info-connected)
* [Feature Screens](#feature-screens)
* [Tools Screen](#tools-screen)
* [Apps Screen](#apps-screen)
* [Resources Screen](#resources-screen)
* [Prompts Screen](#prompts-screen)
* [Logging Screen](#logging-screen)
@ -40,7 +41,7 @@
```
┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ [Server Name ▼] ● Connected (23ms) [Tools] [Resources] [Prompts] [Logs] [Tasks] [History] [Disconnect] │
│ [Server Name ▼] ● Connected (23ms) [Tools] [Apps] [Resources] [Prompts] [Logs] [Tasks] [History] [Disconnect] │
├─────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Full-width content │
@ -49,7 +50,7 @@
```
- **Menu bar** displays server name, connection status with latency, and disconnect button when connected
- **Top navigation** links to Tools, Resources, Prompts, Logs, Tasks, and History sections
- **Top navigation** links to Tools, Apps, Resources, Prompts, Logs, Tasks, and History sections
- **Connection indicator** shows latency from periodic `ping` requests
- **No sidebar** - content areas use full width
@ -399,6 +400,95 @@ When a tool execution triggers sampling or elicitation requests, they appear inl
- Audio player for audio content (base64)
- Resource links displayed as clickable references
#### Apps Screen
A two-panel layout showing the subset of tools that are **MCP Apps** — tools whose
metadata advertises a UI resource via `_meta.ui.resourceUri` (or the deprecated
flat `_meta["ui/resourceUri"]`). The right panel toggles between an input form
(before launch) and the embedded app (after launch).
**Input Form State:**
```
┌──────────────────────────┬──────────────────────────────────────────────┐
│ MCP Apps (1) 🔍 │ get-cohort-data ✕ │
│ [Refresh Apps] │ Returns cohort retention heatmap data... │
├──────────────────────────┼──────────────────────────────────────────────┤
│ │ │
│ ▸ get-cohort-data │ ┌──────────────────────────────────────────┐ │
│ Returns cohort │ │ App Input │ │
│ retention heatmap... │ │ │ │
│ │ │ metric │ │
│ │ │ ┌──────────────────────────────────────┐ │ │
│ │ │ │ retention ▾ │ │ │
│ │ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ periodType │ │
│ │ │ ┌──────────────────────────────────────┐ │ │
│ │ │ │ monthly ▾ │ │ │
│ │ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ cohortCount │ │
│ │ │ ┌──────────────────────────────────────┐ │ │
│ │ │ │ 12 │ │ │
│ │ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ maxPeriods │ │
│ │ │ ┌──────────────────────────────────────┐ │ │
│ │ │ │ 12 │ │ │
│ │ │ └──────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ [▶ Open App] │ │
│ │ └──────────────────────────────────────────┘ │
└──────────────────────────┴──────────────────────────────────────────────┘
```
**Running App State:**
After "Open App" the right panel swaps to the embedded MCP App. The input
form is replaced by the app iframe; a "Back to Input" button (only when the
selected app has input fields) returns to the form.
```
┌──────────────────────────┬──────────────────────────────────────────────┐
│ MCP Apps (1) 🔍 │ get-cohort-data ⛶ ✕ │
│ [Refresh Apps] │ [Back to Input] │
├──────────────────────────┼──────────────────────────────────────────────┤
│ ▸ get-cohort-data │ ┌──────────────────────────────────────────┐ │
│ Returns cohort │ │ │ │
│ retention heatmap... │ │ [Embedded MCP App] │ │
│ │ │ │ │
│ │ │ Cohort Retention Analysis Metric: ▾ │ │
│ │ │ │ │
│ │ │ May 2025 100 85 72 ... │ │
│ │ │ Jun 2025 100 85 78 ... │ │
│ │ │ ... │ │
│ │ │ │ │
│ │ └──────────────────────────────────────────┘ │
└──────────────────────────┴──────────────────────────────────────────────┘
```
**Features:**
- **App Detection** — A tool is treated as an App when `getToolUiResourceUri(tool)`
returns a defined `ui://...` URI (helper from `@modelcontextprotocol/ext-apps`).
Filtering happens upstream (wiring layer); the screen receives an already-filtered
`tools: Tool[]` array of apps.
- **List Changed Indicator** — Shows when `notifications/tools/list_changed` received.
- **Refresh Apps** button re-runs `tools/list`.
- Searchable list, mirroring the Tools screen sidebar.
- **App icons** rendered from `tool.icons` when present.
- **App Input form** generated from `tool.inputSchema` (same `SchemaForm` used by
Tools screen).
- **Open App** button:
- If the tool has input fields, the form must be filled before launch.
- If the tool has no input fields, "Open App" runs immediately on selection.
- **Back to Input** returns to the form (only visible when the app has fields).
- **Maximize / Minimize** toggle hides the sidebar to give the app the full viewport.
- **App Renderer** embeds the UI resource in a sandboxed iframe and bridges
it to the active MCP server via `AppBridge`. Tool input and result are sent
to the app via `sendToolInput` / `sendToolResult`.
- **Close (✕)** deselects the app and clears any in-flight launch.
#### Resources Screen
```

View file

@ -9,6 +9,7 @@ Each feature screen uses a **resizable panel layout** for flexibility.
## Table of Contents
* [Tools Screen](#tools-screen)
* [Apps Screen](#apps-screen)
* [Resources Screen](#resources-screen)
* [Prompts Screen](#prompts-screen)
* [Logging Screen](#logging-screen)
@ -124,6 +125,111 @@ When a tool execution triggers sampling or elicitation requests, they appear inl
- Audio player for audio content (base64)
- Resource links displayed as clickable references
## Apps Screen
A two-panel layout showing the subset of tools that are **MCP Apps** — tools
whose metadata advertises a UI resource via `_meta.ui.resourceUri` (or the
deprecated flat `_meta["ui/resourceUri"]`). The right panel toggles between an
input form (before launch) and the embedded app (after launch).
```
+--------------------------+----------------------------------------------+
| MCP Apps (1) [Q] | get-cohort-data [x] |
| [Refresh Apps] | Returns cohort retention heatmap data |
+--------------------------+----------------------------------------------+
| | |
| > get-cohort-data | +------------------------------------------+ |
| Returns cohort | | App Input | |
| retention heatmap... | | | |
| | | metric | |
| | | +--------------------------------------+ | |
| | | | retention v | | |
| | | +--------------------------------------+ | |
| | | | |
| | | periodType | |
| | | +--------------------------------------+ | |
| | | | monthly v | | |
| | | +--------------------------------------+ | |
| | | | |
| | | cohortCount | |
| | | +--------------------------------------+ | |
| | | | 12 | | |
| | | +--------------------------------------+ | |
| | | | |
| | | maxPeriods | |
| | | +--------------------------------------+ | |
| | | | 12 | | |
| | | +--------------------------------------+ | |
| | | | |
| | | [> Open App] | |
| | +------------------------------------------+ |
+--------------------------+----------------------------------------------+
```
**Running App State:**
After "Open App" the right panel swaps to the embedded MCP App. The input form
is replaced by the app iframe. A "Back to Input" button is shown when the
selected app has input fields; it returns to the form. A Maximize toggle hides
the sidebar so the app takes the full viewport.
```
+--------------------------+----------------------------------------------+
| MCP Apps (1) [Q] | get-cohort-data [^] [x] |
| [Refresh Apps] | [Back to Input] |
+--------------------------+----------------------------------------------+
| > get-cohort-data | +------------------------------------------+ |
| Returns cohort | | | |
| retention heatmap... | | [Embedded MCP App] | |
| | | | |
| | | Cohort Retention Analysis Metric: [v] | |
| | | | |
| | | May 2025 100 85 72 ... | |
| | | Jun 2025 100 85 78 ... | |
| | | ... | |
| | | | |
| | +------------------------------------------+ |
+--------------------------+----------------------------------------------+
```
**Features:**
- **App Detection** — A tool is treated as an App when `getToolUiResourceUri(tool)`
returns a defined `ui://...` URI (helper from `@modelcontextprotocol/ext-apps`).
The detection helper is wrapped in `core/mcp/apps.ts` (e.g. `isAppTool(tool)`)
so all clients (web, CLI, TUI) share one definition. Filtering happens upstream
(wiring layer); the screen receives an already-filtered `tools: Tool[]` array.
- **List Changed Indicator** — shows when `notifications/tools/list_changed`
received; same component as the Tools screen.
- **Refresh Apps** re-runs `tools/list`.
- **Searchable list**, mirroring the Tools screen sidebar.
- **App icons** rendered from `tool.icons` (real MCP `Tool.icons`) when present.
- **App Input form** generated from `tool.inputSchema` via the same `SchemaForm`
used by the Tools screen.
- **Open App** button:
- If the tool has input fields, the form is filled before launch.
- If the tool has no input fields, "Open App" runs immediately on selection.
- **Back to Input** returns to the form (visible only when the app has fields).
- **Maximize / Minimize** toggles full-viewport mode by hiding the sidebar.
- **App Renderer** embeds the UI resource in a sandboxed iframe and bridges
it to the active MCP server via the `AppBridge` from
`@modelcontextprotocol/ext-apps`. The host pushes tool input and results to
the app via `sendToolInput` / `sendToolResult` through an imperative ref.
- **Close (x)** deselects the app and clears any in-flight launch.
**Component contract (presentational):**
The screen and its sub-components stay "dumb": they accept data + callbacks +
imperative refs and contain only display logic.
- `AppsScreen` props: `tools: Tool[]` (already filtered to apps), `listChanged`,
`onRefreshList`, `onSelectApp`, `onOpenApp(name, args)`, `onCloseApp`, plus a
ref for the embedded `AppRenderer`.
- `AppRenderer` exposes an imperative ref with `sendToolInput`,
`sendToolResult`, `sendToolCancelled`, `teardown`. The wiring layer creates
the `AppBridge` from the active MCP `Client` (real MCP type) and drives the
ref; the renderer itself only owns the iframe and the bridge handle.
## Resources Screen
Uses an **accordion layout** for the left panel to organize Resources, Templates, and Subscriptions into collapsible sections.

View file

@ -334,6 +334,24 @@ Single tool entry in the sidebar list with name and annotation badges.
---
### 2.3a AppListItem
Single MCP App entry in the sidebar list. Mirrors `ToolListItem` but renders the app's `tool.icons` (real MCP `Tool.icons`) when present and surfaces a chevron affordance.
| Prop | Type | Description |
|------|------|-------------|
| `tool` | `Tool` | App tool (real MCP `Tool` type) |
| `selected` | `boolean` | Whether this app is currently selected |
| `onClick` | `() => void` | Selection callback |
**Mantine:** `UnstyledButton` with `Group` (Icon + label/description + ChevronRight). Reuses the same active-state background highlight pattern as `ToolListItem`.
**Stories:** Default, Selected, WithIcon, NoDescription, LongName
> Alternative: fold icon support into `ToolListItem` and reuse it on the Apps screen. Decide during implementation — see issue "Tool icon support on tool list items".
---
### 2.4 ToolDetailPanel
Tool name, description, annotations display, and generated parameter form.
@ -373,6 +391,50 @@ Displays tool execution results with copy/clear actions.
---
### 2.5a AppDetailPanel
App name, description, optional icon, and generated `App Input` form. Mirrors `ToolDetailPanel` but the action button is **Open App** (no annotations, no progress display, no cancel — execution is delegated to the embedded app).
| Prop | Type | Description |
|------|------|-------------|
| `tool` | `Tool` | App tool (real MCP `Tool` type) |
| `formValues` | `Record<string, unknown>` | Current form values |
| `isOpening` | `boolean` | Whether the app is currently being launched |
| `onFormChange` | `(values: Record<string, unknown>) => void` | Form value change |
| `onOpenApp` | `() => void` | Open App callback |
**Mantine:** `Stack` with icon + `Title` (`tool.title ?? tool.name`), description `Text`, SchemaForm (see 2.12), full-width `Button` with `IconPlayerPlay` for open. Disabled while `isOpening` or while the form has validation errors.
**Stories:** NoFields, SimpleStringParam, MultipleParams, WithIcon, Opening, ComplexSchema
---
### 2.5b AppRenderer
Embeds an MCP App in a sandboxed iframe and exposes an imperative ref so the wiring layer can drive the bridge. The component is a thin shell — it owns the iframe element and the `AppBridge` lifecycle but does **not** know about the active MCP `Client`. The wiring layer constructs the bridge (real MCP `Client` + `Implementation` + `McpUiHostCapabilities`) and passes it in via the `bridgeFactory` callback when the iframe mounts.
| Prop | Type | Description |
|------|------|-------------|
| `sandboxPath` | `string` | URL/path of the sandbox proxy iframe |
| `tool` | `Tool` | The selected app tool (for resource URI lookup) |
| `bridgeFactory` | `(iframe: HTMLIFrameElement) => AppBridge` | Wiring layer creates the bridge from the active MCP client |
| `onError` | `(err: Error) => void` | Error notification callback |
**Imperative ref (`AppRendererHandle`):**
| Method | Description |
|--------|-------------|
| `sendToolInput(args)` | Push complete tool arguments to the app |
| `sendToolResult(result)` | Push the `CallToolResult` from the server |
| `sendToolCancelled(reason)` | Notify the app that execution was cancelled |
| `teardown()` | Request graceful shutdown before unmounting |
**Mantine:** `Box` with `iframe` element. No Mantine controls — display logic only. The renderer treats the bridge handle and iframe element as runtime state, not props, but exposes the bridge actions through `useImperativeHandle`.
**Stories:** Loading, Loaded (with mock bridge), Error, Maximized
---
### 2.6 ResourceListItem
Single resource entry with name and annotation badges.
@ -855,6 +917,28 @@ Three-panel layout: tool list, detail/form, and results.
---
### 3.2a AppsScreen
Two-panel layout: app list on the left, dynamic right pane that toggles between an input form (before launch) and the embedded `AppRenderer` (after launch). Receives the **already-filtered** subset of tools that are MCP Apps; filtering happens in the wiring layer via the shared `isAppTool` helper in `core/mcp/apps`.
| Prop | Type | Description |
|------|------|-------------|
| `tools` | `Tool[]` | Apps (already filtered to those with `_meta.ui.resourceUri`) |
| `listChanged` | `boolean` | List changed indicator |
| `sandboxPath` | `string` | Sandbox proxy path passed through to `AppRenderer` |
| `bridgeFactory` | `(iframe) => AppBridge` | Wiring layer creates the bridge from the active MCP client |
| `rendererRef` | `Ref<AppRendererHandle>` | Imperative ref forwarded to the embedded `AppRenderer` |
| `onRefreshList` | `() => void` | Refresh apps list |
| `onSelectApp` | `(name: string) => void` | App selection callback |
| `onOpenApp` | `(name: string, args: Record<string, unknown>) => void` | Open App callback (wiring runs `tools/call` and pushes result via the ref) |
| `onCloseApp` | `() => void` | Close currently-open app |
**Mantine:** Two-column layout. Left: `Card` with ListChangedIndicator, search `TextInput`, AppListItem stack, "Refresh Apps" `Button`. Right: `Card` whose content swaps between AppDetailPanel (form state) and AppRenderer (running state) plus a "Back to Input" / Maximize / Close header. Maximize hides the sidebar by widening the right card to full width.
**Stories:** NoSelection, AppSelected, AppRunning, AppRunningMaximized, NoFieldsApp (auto-launches on select), WithListChanged, Empty (no apps available)
---
### 3.3 ResourcesScreen
Two-panel layout: resource list (with accordion sections) and content preview.
@ -1022,7 +1106,8 @@ Depends on Phase 1 elements being complete.
2. **ServerCard** (2.1) - Uses StatusIndicator, TransportBadge, ConnectionToggle, CopyButton, InlineError
3. **AddServerMenu** (2.2)
4. **ToolListItem** (2.3) - Uses AnnotationBadge
5. **ResourceListItem** (2.6) - Uses AnnotationBadge
5. **AppListItem** (2.3a) - Renders `tool.icons` (or fold into ToolListItem)
6. **ResourceListItem** (2.6) - Uses AnnotationBadge
6. **LogControls** (2.11)
7. **ResultPanel** (2.5) - Uses ContentViewer, CopyButton
8. **PromptMessagesDisplay** (2.10) - Uses MessageBubble
@ -1033,17 +1118,19 @@ Depends on Phase 1 elements being complete.
Depends on SchemaForm and Phase 2.
1. **ToolDetailPanel** (2.4) - Uses SchemaForm, AnnotationBadge, ProgressDisplay
2. **ResourcePreviewPanel** (2.7) - Uses ContentViewer
3. **ResourceTemplateInput** (2.8)
4. **PromptArgumentsForm** (2.9)
5. **TaskCard** (2.13) - Uses ProgressDisplay
6. **HistoryEntry** (2.14)
7. **SamplingRequestPanel** (2.15) - Uses MessageBubble
8. **ElicitationFormPanel** (2.16) - Uses SchemaForm
9. **ElicitationUrlPanel** (2.17)
10. **ServerSettingsForm** (2.20)
11. **ImportServerJsonPanel** (2.21)
12. **ExperimentalFeaturesPanel** (2.22)
2. **AppDetailPanel** (2.5a) - Uses SchemaForm
3. **AppRenderer** (2.5b) - Imperative ref shell for embedded MCP App; depends on `@modelcontextprotocol/ext-apps`
4. **ResourcePreviewPanel** (2.7) - Uses ContentViewer
5. **ResourceTemplateInput** (2.8)
6. **PromptArgumentsForm** (2.9)
7. **TaskCard** (2.13) - Uses ProgressDisplay
8. **HistoryEntry** (2.14)
9. **SamplingRequestPanel** (2.15) - Uses MessageBubble
10. **ElicitationFormPanel** (2.16) - Uses SchemaForm
11. **ElicitationUrlPanel** (2.17)
12. **ServerSettingsForm** (2.20)
13. **ImportServerJsonPanel** (2.21)
14. **ExperimentalFeaturesPanel** (2.22)
### Phase 4: Inline Handlers
Depends on Phase 3 panels.
@ -1057,11 +1144,12 @@ Depends on all groups.
1. **ServerListScreen** (3.1)
2. **ToolsScreen** (3.2)
3. **ResourcesScreen** (3.3)
4. **PromptsScreen** (3.4)
5. **LoggingScreen** (3.5)
6. **TasksScreen** (3.6)
7. **HistoryScreen** (3.7)
3. **AppsScreen** (3.2a)
4. **ResourcesScreen** (3.3)
5. **PromptsScreen** (3.4)
6. **LoggingScreen** (3.5)
7. **TasksScreen** (3.6)
8. **HistoryScreen** (3.7)
### Phase 6: Views
Depends on screens.