Skip to content

Commit 8ff9c6a

Browse files
committed
Fix MCP apps implementation doc to match actual codebase
Correct multiple inaccuracies found during code review: - MCPAppFrame props (toolArgs, not structuredContent) - ext-apps lifecycle (three notifications, not two) - Generated HTML fallback (generic JSON, not pod-specific) - Message protocol (remove non-existent mcp-app-refresh, add host-context-changed) - Data refresh flow (card header button, not postMessage) - Sandbox attribute (allow-same-origin in ext-apps mode) - CSS file name (mcp-app-card.css, not general-page.css) - Backend endpoints wording (resources is implemented) - Card controls (document minimize/close) - Future improvements (remove already-done item) Made-with: Cursor
1 parent 137ab36 commit 8ff9c6a

1 file changed

Lines changed: 53 additions & 35 deletions

File tree

docs/mcp-apps-implementation.md

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ The `tool_meta.ui.resourceUri` field indicates this tool has an associated MCP A
4949

5050
**5. Iframe persists in conversation** → It's part of that message's response and stays visible as the user continues chatting
5151

52-
**6. User interacts with iframe** (e.g., clicks Refresh button) → Iframe sends `postMessage` to parent window
52+
**6. User clicks Refresh in card header** → MCPAppFrame calls OLS endpoint directly
5353

5454
**7. UI independently calls OLS endpoint:**
5555
```
@@ -162,45 +162,56 @@ Each section is conditionally rendered — for non-MCP tools only `status` and `
162162

163163
This is the main component that renders interactive views. It:
164164

165-
1. **Receives props**: `resourceUri`, `serverName`, `toolName`, `toolContent`, `status`, `structuredContent`
165+
1. **Receives props**: `resourceUri`, `serverName`, `toolName`, `toolArgs`, `toolContent`, `status` (note: `structuredContent` is not a prop — it is fetched via the proxy endpoint during the ext-apps lifecycle)
166166
2. **Loads MCP resource**: Fetches the HTML resource from the MCP server via the OLS proxy; falls back to locally generated HTML if unavailable
167-
3. **Renders in iframe**: Uses `srcDoc` to display the HTML securely
168-
4. **Handles ext-apps protocol**: Responds to `ui/initialize`, sends `ui/notifications/tool-input` (tool arguments) followed by `ui/notifications/tool-result` (tool output), proxies `tools/call` requests from the iframe to OLS, and handles `ui/notifications/size-changed` for auto-sizing
169-
5. **Supports refresh**: Re-calls the tool endpoint when refresh is requested
167+
3. **Renders in iframe**: Uses `srcDoc` to display the HTML securely (sandbox is `allow-scripts allow-same-origin` for ext-apps mode, `allow-scripts` for fallback)
168+
4. **Handles ext-apps protocol**: Responds to `ui/initialize`, sends `ui/notifications/host-context-changed` (theme), `ui/notifications/tool-input` (tool arguments), then `ui/notifications/tool-result` (fresh data from proxy call), proxies `tools/call` and `tools/list` requests from the iframe to OLS, and handles `ui/notifications/size-changed` for auto-sizing
169+
5. **Supports refresh**: Card header button re-calls the tool endpoint and pushes new data into the iframe
170170

171171
#### MCP Apps lifecycle notifications (ext-apps compliance)
172172

173-
When the iframe's ext-apps SDK sends `ui/notifications/initialized`, MCPAppFrame sends two notifications in order:
173+
When the iframe's ext-apps SDK sends `ui/notifications/initialized`, MCPAppFrame sends three notifications in order:
174174

175-
**1. `ui/notifications/tool-input`** — carries the tool's input arguments:
175+
**1. `ui/notifications/host-context-changed`** — sends the current theme:
176+
177+
- `theme`: `"dark"` or `"light"`
178+
179+
This ensures apps using the `onhostcontextchanged` callback can apply the correct theme immediately after initialization.
180+
181+
**2. `ui/notifications/tool-input`** — carries the tool's input arguments:
176182

177183
- `arguments`: the complete tool call arguments from the LLM (e.g., `{ query: "up{}", step: "1m", title: "Uptime" }`)
178184

179185
This matches the [`McpUiToolInputNotification`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiToolInputNotification.html) spec. MCP Apps can use this to display context about what was queried (e.g., chart title, PromQL query string, filter parameters) without needing to embed that information in the tool result.
180186

181-
**2. `ui/notifications/tool-result`** — carries the tool execution result:
187+
**3. `ui/notifications/tool-result`** — carries a fresh tool execution result:
182188

183-
- `content`: `[{ type: "text", text: toolContent }]` (real tool output, not a placeholder)
189+
The component calls the tool **again** via `POST /mcp-apps/tools/call` (not using the already-streamed content) so the iframe receives structured data in the ext-apps format:
190+
191+
- `content`: `[{ type: "text", text: "..." }]` (from the proxy response)
184192
- `structuredContent`: the structured data from the tool result (if available)
185-
- `isError`: `true` when `status === "error"`
193+
- `isError`: `true` when the tool returned an error
194+
195+
If the proxy call fails, MCPAppFrame falls back to the streamed text content: `content: [{ type: "text", text: toolContent }]`.
186196

187197
This matches the [`McpUiToolResultNotification`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiToolResultNotification.html) spec and ensures the iframe's `app.ontoolresult` callback receives real data on first load.
188198

189-
Both notifications are also re-sent on refresh so the app can update headers/context alongside the data.
199+
All three notifications are also re-sent on refresh (triggered by the card header button) so the app can update headers/context alongside the data.
190200

191201
#### Immutable.js caveat for tool arguments
192202

193203
Tool arguments are stored in Redux via Immutable.js `mergeIn`, which deep-converts plain objects into Immutable Maps. When extracting `toolArgs` from the store, `.toJS()` must be called to convert back to a plain JS object — `postMessage` uses the structured clone algorithm which cannot clone Immutable class instances, resulting in empty/mangled objects in the iframe.
194204

195205
The conversion is wrapped in `React.useMemo` (keyed on the raw Immutable Map reference) to avoid creating a new object on every render. Without memoization, every Redux state change would produce a new `toolArgs` reference, causing `MCPAppFrame`'s `useEffect` hooks to re-fire and reload iframe content.
196206

197-
#### Generated HTML Features
207+
#### Generated HTML Fallback
198208

199-
- **Summary cards**: Total pods, average CPU, average memory
200-
- **Data table**: Pod names with CPU/memory bars (color-coded by severity)
201-
- **Refresh button**: Triggers data reload via `postMessage`
202-
- **Auto-resize**: Iframe height adjusts to content (starts small, grows/shrinks via resize messages, clamped to 60–960px)
203-
- **Theme support**: Adapts to dark/light mode
209+
When the MCP server's HTML resource is unavailable, MCPAppFrame falls back to calling the tool via the proxy endpoint and rendering the result generically:
210+
211+
- **`generateGenericDataHtml`**: Renders `structuredContent` (or the raw response) as formatted JSON in a `<pre>` block with the tool name as a heading
212+
- **`wrapHtmlContent`**: Wraps raw HTML text content in a minimal styled page (used when no structured data is available)
213+
- **Auto-resize**: Both generated pages include a `ResizeObserver` that sends `mcp-app-resize` messages to auto-size the iframe
214+
- **Theme support**: Background and text colors adapt to dark/light mode via inline styles
204215

205216
#### Iframe Auto-Sizing
206217

@@ -221,41 +232,48 @@ The iframe communicates with the parent via `postMessage`:
221232
|--------------|-----------|---------|
222233
| `ui/initialize` | iframe → parent | Ext-apps SDK initialization request |
223234
| `ui/notifications/initialized` | iframe → parent | Ext-apps SDK ready for data |
235+
| `ui/notifications/host-context-changed` | parent → iframe | Theme and host context updates |
224236
| `ui/notifications/tool-input` | parent → iframe | Tool input arguments (query, title, filters, etc.) |
225237
| `ui/notifications/tool-result` | parent → iframe | Tool execution result with structured content |
226238
| `ui/notifications/size-changed` | iframe → parent | Request iframe height change (ext-apps SDK) |
227239
| `ui/tools/call` | iframe → parent | Iframe requests a tool call via host proxy |
228240
| `ui/tools/list` | iframe → parent | Iframe requests available tools |
229241
| `mcp-app-resize` | iframe → parent | Request iframe height change (generated HTML) |
230-
| `mcp-app-refresh` | iframe → parent | Request data refresh (generated HTML) |
231242

232243
### 6. Data Refresh Flow
233244

245+
Refresh is triggered by the **card header button** (SyncAltIcon), not by a postMessage from the iframe.
246+
234247
```
235-
User clicks Refresh button in iframe
248+
User clicks Refresh button in card header
236249
237-
iframe sends postMessage({ type: 'mcp-app-refresh' })
238-
239-
MCPAppFrame.handleRefreshData() called
250+
MCPAppFrame.handleRefresh() called
240251
241252
POST /api/proxy/plugin/lightspeed-console-plugin/ols/v1/mcp-apps/tools/call
242253
243254
Backend calls MCP server tool
244255
245-
New structured_content returned
256+
New result returned (content + structured_content)
246257
247-
MCPAppFrame regenerates HTML with new data
258+
ext-apps mode: sends ui/notifications/tool-input + ui/notifications/tool-result to iframe
259+
fallback mode: regenerates HTML with new data via generateGenericDataHtml
248260
```
249261

250-
### 7. Expand/Collapse Functionality
262+
### 7. Card Controls
263+
264+
The card header includes four action buttons:
251265

252-
The card header includes an expand button that toggles between:
253-
- **Normal**: Card inline within chat messages
254-
- **Expanded**: Card fills the entire chat panel (absolute positioning)
266+
| Button | Icon | Behavior |
267+
|--------|------|----------|
268+
| Refresh | `SyncAltIcon` | Re-calls the tool via proxy and pushes new data |
269+
| Expand / Collapse | `ExpandIcon` / `CompressIcon` | Toggles between inline and full-panel mode |
270+
| Minimize | `MinusIcon` | Collapses to a compact title bar (restore via `WindowRestoreIcon`) |
271+
| Close | `TimesIcon` | Dismisses the card entirely (not recoverable) |
255272

256273
CSS classes:
257-
- `.ols-plugin__mcp-app-card` - Base styles
258-
- `.ols-plugin__mcp-app-card--expanded` - Full-panel overlay
274+
- `.ols-plugin__mcp-app-card` — Base styles
275+
- `.ols-plugin__mcp-app-card--expanded` — Full-panel overlay (absolute positioning)
276+
- `.ols-plugin__mcp-app-card--minimized` — Compact title bar with reduced opacity
259277

260278
## Technical Decisions
261279

@@ -268,7 +286,7 @@ MCPAppFrame supports two approaches, tried in order:
268286

269287
### Why Use iframe with srcDoc?
270288

271-
- **Security**: `sandbox="allow-scripts"` prevents XSS attacks
289+
- **Security**: `sandbox="allow-scripts"` (fallback) or `sandbox="allow-scripts allow-same-origin"` (ext-apps mode, needed for SDK origin checks) prevents XSS attacks
272290
- **Isolation**: Styles don't leak between chat and visualization
273291
- **Flexibility**: Can render any HTML content
274292

@@ -282,8 +300,8 @@ The implementation relies on these OLS backend endpoints:
282300

283301
| Endpoint | Purpose |
284302
|----------|---------|
285-
| `POST /v1/mcp-apps/tools/call` | Call an MCP tool directly |
286-
| `POST /v1/mcp-apps/resources` | Fetch UI resource HTML (for future ext-apps support) |
303+
| `POST /v1/mcp-apps/tools/call` | Call an MCP tool directly (from card refresh or ext-apps `tools/call`) |
304+
| `POST /v1/mcp-apps/resources` | Fetch `ui://` HTML resource from an MCP server (for ext-apps iframe rendering) |
287305

288306
### Tool Call Request
289307

@@ -318,7 +336,8 @@ The implementation relies on these OLS backend endpoints:
318336
| `src/components/ResponseTools.tsx` | Render MCPAppFrame for tool results with UI; color-coded tool labels (red/yellow/blue/grey) |
319337
| `src/components/ResponseToolModal.tsx` | Updated to show all tool fields (metadata, content, structured content) |
320338
| `src/components/Prompt.tsx` | Extract tool results from streaming |
321-
| `src/components/general-page.css` | MCP app card styles, tool modal metadata/section styles |
339+
| `src/components/mcp-app-card.css` | MCP app card, expanded, and minimized styles |
340+
| `src/components/general-page.css` | Tool modal metadata/section styles |
322341
| `src/types.ts` | Tool type definition (status includes `'truncated'`) |
323342
| `locales/en/plugin__lightspeed-console-plugin.json` | i18n strings |
324343

@@ -327,7 +346,6 @@ The implementation relies on these OLS backend endpoints:
327346
1. **Caching**: Cache tool results to avoid redundant calls
328347
2. **Error handling**: Better error states and retry mechanisms
329348
3. **Accessibility**: Ensure ARIA labels and keyboard navigation
330-
4. **Host context updates**: Send `ui/notifications/host-context-changed` on theme changes
331349

332350
## Testing
333351

0 commit comments

Comments
 (0)