You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs: address review feedback on design-guidelines, patterns, troubleshooting
- design-guidelines: reframe scope bullets around no-nested-scroll / narrow-width
layout / no multi-page nav; clarify host-UI imitation vs native styling tension;
drop "height-constrained" inline claim; point to containerDimensions via
onhostcontextchanged; note App mounts before tool inputs are sent
- patterns: correct structuredContent model-visibility (shown iff content empty);
rework touch-action guidance so inline never blocks vertical scroll; add
sendSizeChanged to fixed-height recipe; document getUiCapability for per-client
tool registration; add downloadFile + capability fencing alongside openLink
- troubleshooting: lead blank-iframe list with missing connect(); drop
hypothetical window.claude reference
Copy file name to clipboardExpand all lines: docs/design-guidelines.md
+8-8Lines changed: 8 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -24,19 +24,19 @@ A title inside the content area (for example, "Q3 Revenue by Region" above a cha
24
24
25
25
An MCP App answers one question or supports one task. Avoid building a full dashboard with tabs, sidebars, and settings panels.
26
26
27
-
- Inline mode should fit within roughly one viewport of scroll. Content that is significantly taller than the chat viewport belongs in fullscreen mode, or should be trimmed.
28
-
-Limit inline mode to one primary action. A "Confirm" button is appropriate; a toolbar with eight icons is not.
29
-
-Let the conversation handle navigation. Rather than adding a search box inside the App, let the user ask a follow-up question that re-invokes the tool with new arguments.
27
+
- Inline content can be tall, but it must scroll with the surrounding conversation. Do not introduce nested scroll containers in inline mode; a scrollable region inside a scrollable chat is difficult to use on every input device.
28
+
-Design the inline layout to remain usable at narrow widths. Chat columns can be as narrow as a mobile message bubble, so dense toolbars and side-by-side panels should collapse or move to fullscreen mode rather than overflow.
29
+
-Avoid multi-page navigation (routes, wizards, tab stacks) in inline mode. The conversation already provides history and back-navigation. In-App search or filtering over the current data set is fine; navigating to a different document or view is better handled by a follow-up tool call, or reserved for fullscreen mode.
30
30
31
31
## Host UI imitation
32
32
33
-
Your App must not resemble the surrounding chat client. Do not render:
33
+
Use host-provided styles so your App matches the surrounding theme, but keep the boundary between App content and host UI unambiguous. Do not render:
34
34
35
35
- Chat bubbles or message threads
36
36
- Anything that resembles the host's text input or send button
37
37
- System notifications or permission dialogs
38
38
39
-
These patterns blur the line between host UI and App content, and most hosts prohibit them in their submission guidelines.
39
+
A user must never mistake App-rendered surfaces for host controls. Most hosts prohibit these patterns in their submission guidelines.
40
40
41
41
## Host styling
42
42
@@ -46,14 +46,14 @@ Brand colors are appropriate for content elements such as chart series or status
46
46
47
47
## Display modes
48
48
49
-
Design for inline mode first. It is the default, and it is narrow (often the width of a chat message) and height-constrained.
49
+
Design for inline mode first. It is the default, and it is narrow (often the width of a chat message). Inline height is effectively unconstrained: hosts apply only a high safety cap, so the iframe will grow to whatever content height the App reports.
50
50
51
51
Treat fullscreen as a progressive enhancement for Apps that benefit from more space: editors, maps, large datasets. Check `hostContext.availableDisplayModes` before rendering a fullscreen toggle, since not every host supports it.
52
52
53
-
When the display mode changes, update your layout: remove edge border radius, expand to fill the viewport, and re-read `containerDimensions`from the updated host context.
53
+
When the display mode changes, update your layout: remove edge border radius and expand to fill the viewport. To size the App to the space the host provides, subscribe to `hostContext.containerDimensions`via {@link app!App.onhostcontextchanged `onhostcontextchanged`} and apply the reported width and height to your root element.
54
54
55
55
## Loading and empty states
56
56
57
-
The App mounts before the tool result arrives. Between `ui/initialize` and `ontoolresult`, render a loading indicator such as a skeleton, spinner, or neutral background. A blank rectangle looks broken.
57
+
The App mounts before the tool result arrives, and even before the tool inputs are sent. Between `ui/initialize` and `ontoolresult`, render a loading indicator such as a skeleton, spinner, or neutral background. A blank rectangle looks broken.
58
58
59
59
If the tool result can be empty (no search results, empty cart), design an explicit empty state rather than rendering nothing.
|`content`| Yes | Yes | Short text summary for the model and for text-only hosts |
47
+
|`structuredContent`|Only when `content` is empty| Yes | Structured data the App renders (tables, charts, lists) |
48
+
|`_meta`| No | Yes | Opaque metadata such as IDs, timestamps, and view identifiers |
49
49
50
-
Keep `content`brief. The model uses it to decide what to say next, so a one-line summary is preferable to raw data.
50
+
Per the MCP guideline, hosts pass `structuredContent` to the model only when `content`is omitted, so populating `content` is the way to keep large render payloads out of the model's context. The App receives all three fields regardless. Keep `content` brief: the model uses it to decide what to say next, so a one-line summary is preferable to raw data.
51
51
52
52
> [!WARNING]
53
-
> Do not return large payloads in tool results. Serve base64-encoded audio, images, or file contents via MCP resources (see [Serving binary blobs via resources](#serving-binary-blobs-via-resources)) or have the App fetch them over the network. Although `structuredContent` is excluded from the model's context by the specification, large tool results still slow down transport, inflate conversation storage, and some host implementations include more of the result than the specification requires.
53
+
> Do not return large payloads in tool results. Serve base64-encoded audio, images, or file contents via MCP resources (see [Serving binary blobs via resources](#serving-binary-blobs-via-resources)) or have the App fetch them over the network. Even when `structuredContent` is kept out of the model's context, large tool results still slow down transport, inflate conversation storage, and some host implementations include more of the result than the specification requires.
54
54
55
-
Write `content` for the model, not the user. The user sees your App, not the `content` text. Use `content` to tell the model what happened so it can respond without repeating what is already on screen:
55
+
Write `content` for the model, not the user. The user sees your App, not the `content` text. State explicitly that a view was displayed and what it contains so the model does not re-describe what is already on screen:
56
56
57
57
```ts
58
58
return {
@@ -436,21 +436,23 @@ function MyApp() {
436
436
437
437
## Supporting touch devices
438
438
439
-
Apps that handle pointer gestures (pan, drag, pinch) must prevent those gestures from also scrolling the surrounding chat. Set [`touch-action`](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action) on interactive surfaces:
439
+
In inline mode, the App scrolls with the surrounding conversation. Do not capture vertical pan gestures or add nested scroll containers to inline layouts; let touch-drag pass through so the user can scroll the chat past your App.
440
+
441
+
For interactive surfaces that handle horizontal drag or pinch (sliders, canvases, maps), set [`touch-action`](https://developer.mozilla.org/en-US/docs/Web/CSS/touch-action) so the browser delivers `pointermove` events to the App while still allowing vertical scroll to reach the page:
440
442
441
443
```css
442
-
/*Chart or canvas that handles its own panning*/
443
-
.chart-surface {
444
-
touch-action: none;
444
+
/*Horizontal slider: consume horizontal drag, leave vertical to the chat*/
445
+
.slider-track {
446
+
touch-action: pan-y;
445
447
}
446
448
447
-
/*Horizontal slider that should not trigger vertical page scroll*/
/*Fullscreen canvas that handles its own pan and pinch*/
450
+
.fullscreen.chart-surface {
451
+
touch-action: none;
450
452
}
451
453
```
452
454
453
-
Without`touch-action`, dragging across the App on a mobile device also scrolls the chat, and the App may never receive `pointermove` events.
455
+
Reserve`touch-action: none` for fullscreen mode, where the App owns the viewport and there is no outer scroll to conflict with.
454
456
455
457
Prevent horizontal overflow by setting `overflow-x: hidden` on the root container if the layout contains any fixed-width elements. Horizontal overflow on mobile causes the entire App to shift when the page is scrolled.
456
458
@@ -513,14 +515,16 @@ There are three height strategies:
513
515
514
516
**Auto-resize (default).** For content with a natural height. The iframe grows to fit. Do not set `height: 100vh` or `height: 100%` on the root element; doing so creates a feedback loop where the reported height keeps increasing.
515
517
516
-
**Fixed height.** For UI that should remain the same size when inline. Disable auto-resize and set an explicit height:
518
+
**Fixed height.** For UI that should remain the same size when inline. Disable auto-resize, set an explicit height, and report it to the host with {@link app!App.sendSizeChanged `sendSizeChanged`} so the iframe is allocated the correct size:
The tool-to-resource binding is declared at registration time. A tool either has a `_meta.ui.resourceUri` or it does not; the server cannot decide per-call whether to render UI.
729
733
730
-
If both behaviors are needed, register two tools:
734
+
A stateful server can decide at connection time. During `initialize`, the client declares whether it supports the Apps extension; use {@link server-helpers!getUiCapability `getUiCapability`} to read that declaration and register UI-backed tools only for clients that can render them:
If both behaviors are needed for a single UI-capable client, register two tools:
731
767
732
768
-`query-data` with no `_meta.ui`, returning text and structured data for the model to reason about
733
769
-`visualize-data` with `_meta.ui`, returning the same data rendered as an interactive App
@@ -736,16 +772,35 @@ Write distinct descriptions so the model selects the correct tool based on user
736
772
737
773
If the decision must be made server-side (for example, showing UI only when the result set exceeds a threshold), the workaround is to always attach the UI resource and have the App render a minimal collapsed placeholder when there is nothing to show. Keep the placeholder small to avoid adding visual noise to the conversation.
738
774
739
-
## Opening external links
775
+
## Opening external links and downloading files
740
776
741
-
Use {@link app!App.openLink `app.openLink()`} instead of `window.open()` or `<a target="_blank">`. The sandbox blocks direct navigation; `openLink` asks the host to open the URL on the App's behalf.
777
+
Use {@link app!App.openLink `app.openLink()`} instead of `window.open()` or `<a target="_blank">`, and {@link app!App.downloadFile `app.downloadFile()`} instead of synthesizing `<a download>` clicks. The sandbox blocks direct navigation and downloads; these methods ask the host to perform the action on the App's behalf.
742
778
743
-
Hosts typically show an interstitial confirmation so users can review the destination before navigating. Do not assume navigation is instant, and do not chain multiple `openLink` calls.
779
+
Both are optional host capabilities. Check {@link app!App.getHostCapabilities `getHostCapabilities`} before rendering the corresponding controls so the App degrades gracefully on hosts that do not implement them:
Hosts typically show an interstitial confirmation for `openLink` so users can review the destination before navigating. Do not assume navigation is instant, and do not chain multiple `openLink` calls.
803
+
749
804
## Lowering perceived latency
750
805
751
806
Use {@link app!App.ontoolinputpartial `ontoolinputpartial`} to receive streaming tool arguments as they arrive. This lets you show a loading preview before the complete input is available, such as streaming code into a `<pre>` tag before executing it, partially rendering a table as data arrives, or incrementally populating a chart.
Copy file name to clipboardExpand all lines: docs/troubleshooting.md
+7-5Lines changed: 7 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,13 +10,15 @@ description: Diagnose common MCP App issues including blank iframes, CSP errors,
10
10
11
11
The most common causes, in the order you should check them:
12
12
13
-
1.**Uncaught JavaScript error.**Open browser developer tools inside the iframe: right-click the App area, choose _Inspect_, then switch the console context dropdown (top-left of the Console tab) from `top` to the sandboxed frame. An uncaught error stops the App before it paints.
13
+
1.**`connect()` was never called.**The host waits for the App to send `ui/initialize` before giving the iframe a non-zero size, so an App that constructs `new App(...)` but never calls `app.connect(transport)` renders as an empty sliver. Confirm `connect()` runs on page load and that its promise resolves.
14
14
15
-
2.**CSP violation.**Look for `Refused to connect to…` or `Refused to load…` in the console. Any network request, including to `localhost` during development, must be declared in `_meta.ui.csp.connectDomains` or `resourceDomains`. See the [CSP & CORS guide](./csp-cors.md).
15
+
2.**Uncaught JavaScript error.**Open browser developer tools inside the iframe: right-click the App area, choose _Inspect_, then switch the console context dropdown (top-left of the Console tab) from `top` to the sandboxed frame. An uncaught error stops the App before it paints.
16
16
17
-
3.**Resource URI mismatch.**The `_meta.ui.resourceUri` on the tool must match the URI passed to `registerAppResource` exactly. A trailing slash or case difference prevents the host from finding the HTML.
17
+
3.**CSP violation.**Look for `Refused to connect to…` or `Refused to load…` in the console. Any network request, including to `localhost` during development, must be declared in `_meta.ui.csp.connectDomains` or `resourceDomains`. See the [CSP & CORS guide](./csp-cors.md).
18
18
19
-
4.**Wrong MIME type.** The resource's `mimeType` must be `text/html;profile=mcp-app` (exported as {@link app!RESOURCE_MIME_TYPE `RESOURCE_MIME_TYPE`}). Plain `text/html` is not recognized as an App resource.
19
+
4.**Resource URI mismatch.** The `_meta.ui.resourceUri` on the tool must match the URI passed to `registerAppResource` exactly. A trailing slash or case difference prevents the host from finding the HTML.
20
+
21
+
5.**Wrong MIME type.** The resource's `mimeType` must be `text/html;profile=mcp-app` (exported as {@link app!RESOURCE_MIME_TYPE `RESOURCE_MIME_TYPE`}). Plain `text/html` is not recognized as an App resource.
20
22
21
23
## `ontoolinput` / `ontoolresult` never fires
22
24
@@ -28,7 +30,7 @@ The most common causes, in the order you should check them:
28
30
29
31
MCP Apps are portable only if they use the SDK exclusively. Common portability mistakes:
30
32
31
-
-**Host-specific globals.** Do not reference `window.openai`, `window.claude`, or any other host-injected object. Use the `App` class from this SDK, which speaks the standard protocol to any compliant host.
33
+
-**Host-specific globals.** Do not reference host-injected globals such as `window.openai`. Use the `App` class from this SDK, which speaks the standard protocol to any compliant host.
32
34
-**Hardcoded CDN URLs.** Bundle assets into the App or declare their origins in `resourceDomains`.
33
35
-**Hardcoded sandbox origin.** The origin that serves the App varies by host. Use `_meta.ui.domain` to request a stable origin rather than hardcoding one in CORS allowlists. See [CSP & CORS](./csp-cors.md).
0 commit comments