Skip to content

Commit a14c46d

Browse files
committed
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
1 parent 8cef8ed commit a14c46d

4 files changed

Lines changed: 92 additions & 47 deletions

File tree

docs/design-guidelines.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,19 @@ A title inside the content area (for example, "Q3 Revenue by Region" above a cha
2424

2525
An MCP App answers one question or supports one task. Avoid building a full dashboard with tabs, sidebars, and settings panels.
2626

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.
3030

3131
## Host UI imitation
3232

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:
3434

3535
- Chat bubbles or message threads
3636
- Anything that resembles the host's text input or send button
3737
- System notifications or permission dialogs
3838

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.
4040

4141
## Host styling
4242

@@ -46,14 +46,14 @@ Brand colors are appropriate for content elements such as chart series or status
4646

4747
## Display modes
4848

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.
5050

5151
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.
5252

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.
5454

5555
## Loading and empty states
5656

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.
5858

5959
If the tool result can be empty (no search results, empty cart), design an explicit empty state rather than rendering nothing.

docs/patterns.md

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,18 @@ registerAppTool(
4141

4242
A tool result has three fields for data, each with different visibility:
4343

44-
| Field | Seen by model | Seen by App | Use for |
45-
| ------------------- | ------------- | ----------- | ------------------------------------------------------------- |
46-
| `content` | Yes | Yes | Short text summary for the model and for text-only hosts |
47-
| `structuredContent` | No | Yes | Structured data the App renders (tables, charts, lists) |
48-
| `_meta` | No | Yes | Opaque metadata such as IDs, timestamps, and view identifiers |
44+
| Field | Seen by model | Seen by App | Use for |
45+
| ------------------- | ---------------------------- | ----------- | ------------------------------------------------------------- |
46+
| `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 |
4949

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.
5151

5252
> [!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.
5454
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:
5656

5757
```ts
5858
return {
@@ -436,21 +436,23 @@ function MyApp() {
436436
437437
## Supporting touch devices
438438

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:
440442

441443
```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;
445447
}
446448

447-
/* Horizontal slider that should not trigger vertical page scroll */
448-
.slider-track {
449-
touch-action: pan-y; /* allow vertical scroll, consume horizontal */
449+
/* Fullscreen canvas that handles its own pan and pinch */
450+
.fullscreen .chart-surface {
451+
touch-action: none;
450452
}
451453
```
452454

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.
454456

455457
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.
456458

@@ -513,14 +515,16 @@ There are three height strategies:
513515

514516
**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.
515517

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:
517519

518520
```ts
519521
const app = new App(
520522
{ name: "my-app", version: "0.1.0" },
521523
{},
522524
{ autoResize: false },
523525
);
526+
await app.connect(new PostMessageTransport(window.parent));
527+
app.sendSizeChanged({ width: document.body.clientWidth, height: 500 });
524528
```
525529

526530
```css
@@ -727,7 +731,39 @@ app.ontoolresult = (result) => {
727731

728732
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.
729733

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:
735+
736+
<!-- prettier-ignore -->
737+
```ts source="../src/server/index.examples.ts#getUiCapability_checkSupport"
738+
server.server.oninitialized = () => {
739+
const clientCapabilities = server.server.getClientCapabilities();
740+
const uiCap = getUiCapability(clientCapabilities);
741+
742+
if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
743+
// App-enhanced tool
744+
registerAppTool(
745+
server,
746+
"weather",
747+
{
748+
description: "Get weather information with interactive dashboard",
749+
_meta: { ui: { resourceUri: "ui://weather/dashboard" } },
750+
},
751+
weatherHandler,
752+
);
753+
} else {
754+
// Text-only fallback
755+
server.registerTool(
756+
"weather",
757+
{
758+
description: "Get weather information",
759+
},
760+
textWeatherHandler,
761+
);
762+
}
763+
};
764+
```
765+
766+
If both behaviors are needed for a single UI-capable client, register two tools:
731767

732768
- `query-data` with no `_meta.ui`, returning text and structured data for the model to reason about
733769
- `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
736772

737773
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.
738774

739-
## Opening external links
775+
## Opening external links and downloading files
740776

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.
742778

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:
744780

745781
```ts
746-
await app.openLink({ url: "https://example.com/docs" });
782+
if (app.getHostCapabilities()?.openLinks) {
783+
await app.openLink({ url: "https://example.com/docs" });
784+
}
785+
786+
if (app.getHostCapabilities()?.downloadFile) {
787+
await app.downloadFile({
788+
contents: [
789+
{
790+
type: "resource",
791+
resource: {
792+
uri: "file:///report.csv",
793+
mimeType: "text/csv",
794+
text: csv,
795+
},
796+
},
797+
],
798+
});
799+
}
747800
```
748801

802+
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+
749804
## Lowering perceived latency
750805

751806
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.

docs/troubleshooting.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ description: Diagnose common MCP App issues including blank iframes, CSP errors,
1010

1111
The most common causes, in the order you should check them:
1212

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.
1414

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.
1616

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).
1818

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.
2022

2123
## `ontoolinput` / `ontoolresult` never fires
2224

@@ -28,7 +30,7 @@ The most common causes, in the order you should check them:
2830

2931
MCP Apps are portable only if they use the SDK exclusively. Common portability mistakes:
3032

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.
3234
- **Hardcoded CDN URLs.** Bundle assets into the App or declare their origins in `resourceDomains`.
3335
- **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).
3436

src/generated/schema.json

Lines changed: 0 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)