Skip to content

Commit 0bd03ce

Browse files
committed
docs: address adversarial-review findings
Accuracy: - patterns: drop incorrect useAutoResize-with-element advice; useApp simply doesn't expose autoResize, so construct App manually - patterns: add full host-driven height snippet using addEventListener and the containerDimensions union (with in-guards), plus a strategy decision table - patterns/overview: align structuredContent visibility wording (model sees it only when content is empty) across both pages - design-guidelines/patterns/troubleshooting: switch new content from deprecated on* setters to addEventListener; retitle troubleshooting section to event names - design-guidelines: describe containerDimensions as fixed-or-max bounds, not plain {width,height} Gaps: - design-guidelines: cover toolcancelled as a terminal loading state; mention requestTeardown alongside the no-close-button guidance - troubleshooting: surface basic-host repro tip at top of Blank iframe Clarity/examples: - patterns: openLink/downloadFile snippet now gates control rendering, not the call site; sharing-one-UI snippet guards isError before casting; fixed-height snippet uses bare app.connect(); dedupe 100vh warning - design-guidelines: reword Scope intro to target application shells (resolve tabs contradiction); soften inline-height claim and cross-link to patterns
1 parent 3ce78cd commit 0bd03ce

5 files changed

Lines changed: 54 additions & 35 deletions

File tree

docs/design-guidelines.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,15 @@ Hosts render a frame around your App that typically includes:
1616
- Display-mode controls (expand, collapse, close)
1717
- Attribution indicating which connector or server provided the App
1818

19-
Do not duplicate these elements. Your App does not need its own close button, header bar, or "powered by" footer. Begin the layout with content.
19+
Do not duplicate these elements. Your App does not need its own close button, header bar, or "powered by" footer. Begin the layout with content. If the App should dismiss itself after a task completes, call {@link app!App.requestTeardown `app.requestTeardown()`}; the host decides whether to honor the request.
2020

2121
A title inside the content area (for example, "Q3 Revenue by Region" above a chart) is acceptable. The App's brand name is not.
2222

2323
## Scope
2424

25-
An MCP App answers one question or supports one task. Avoid building a full dashboard with tabs, sidebars, and settings panels.
25+
An MCP App answers one question or supports one task. Avoid building an application shell around it: global navigation, sidebars, and settings panels belong to the host, not the App.
2626

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.
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. See [Supporting touch devices](./patterns.md#supporting-touch-devices).
2828
- 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.
2929
- 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

@@ -46,14 +46,16 @@ 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). Inline height is effectively unconstrained: hosts apply only a high safety cap, so the iframe will grow to whatever content height the App reports.
49+
Design for inline mode first. It is the default, and it is narrow (often the width of a chat message). Most hosts let inline height grow with content up to a high safety cap, but a host may also pin the iframe to a fixed height via `containerDimensions`; see [Controlling App height](./patterns.md#controlling-app-height).
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 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.
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, listen for `hostcontextchanged` via {@link app!App.addEventListener `app.addEventListener`} and read `containerDimensions` from the event payload, which reports either fixed `width`/`height` or `maxWidth`/`maxHeight` bounds depending on the host.
5454

5555
## Loading and empty states
5656

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.
57+
The App mounts before the tool result arrives, and even before the tool inputs are sent. Render a loading indicator such as a skeleton, spinner, or neutral background between `ui/initialize` and the first terminal event. A blank rectangle looks broken.
58+
59+
The terminal events are `toolresult` (success or `isError`) and `toolcancelled` (user stopped the request). Handle both: an App that clears its loading state only on `toolresult` will spin indefinitely if the user cancels.
5860

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

docs/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ sequenceDiagram
9494

9595
1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server.
9696
2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). This handshake ensures the View is ready before receiving data.
97-
3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context.
97+
3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering, passed to the model only when `content` is empty). This separation lets servers provide rich data to the UI without bloating the model's context.
9898
4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context.
9999
5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources.
100100

docs/patterns.md

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ A tool result has three fields for data, each with different visibility:
4747
| `structuredContent` | Only when `content` is empty | Yes | Structured data the App renders (tables, charts, lists) |
4848
| `_meta` | No | Yes | Opaque metadata such as IDs, timestamps, and view identifiers |
4949

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.
50+
Per the core MCP guideline, hosts fall back to passing `structuredContent` to the model only when `content` is empty, 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]
5353
> 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.
@@ -511,9 +511,13 @@ In fullscreen mode, remove the container's border radius so content extends to t
511511

512512
By default, the SDK observes the document's content height and reports it to the host so the iframe grows to fit (`autoResize: true`). This is appropriate for content-driven UI such as cards, tables, and forms. It is the wrong choice for viewport-filling UI such as canvases, maps, and editors.
513513

514-
There are three height strategies:
514+
| UI type | Strategy | `autoResize` | Root CSS height |
515+
| ------------------------------------- | ----------- | ------------ | -------------------------- |
516+
| Cards, tables, forms (natural height) | Auto-resize | `true` | unset |
517+
| Fixed-size widgets | Fixed | `false` | explicit `px` |
518+
| Canvases, maps, editors (fill space) | Host-driven | `false` | from `containerDimensions` |
515519

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.
520+
**Auto-resize (default).** For content with a natural height. The iframe grows to fit.
517521

518522
**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:
519523

@@ -523,7 +527,7 @@ const app = new App(
523527
{},
524528
{ autoResize: false },
525529
);
526-
await app.connect(new PostMessageTransport(window.parent, window.parent));
530+
await app.connect();
527531
app.sendSizeChanged({ width: document.body.clientWidth, height: 500 });
528532
```
529533

@@ -535,12 +539,28 @@ body {
535539
}
536540
```
537541

538-
**Host-driven height.** For UI that should fill the space the host provides (common for fullscreen-capable Apps). Disable auto-resize and read dimensions from {@link types!McpUiHostContext `hostContext.containerDimensions`}, updating on {@link app!App.onhostcontextchanged `onhostcontextchanged`}.
542+
**Host-driven height.** For UI that should fill the space the host provides (common for fullscreen-capable Apps). Disable auto-resize and size the root element from {@link types!McpUiHostContext `hostContext.containerDimensions`}, which may report fixed `width`/`height` or `maxWidth`/`maxHeight` bounds:
543+
544+
```ts
545+
const app = new App(
546+
{ name: "my-app", version: "0.1.0" },
547+
{},
548+
{ autoResize: false },
549+
);
550+
const root = document.getElementById("root")!;
551+
552+
app.addEventListener("hostcontextchanged", (ctx) => {
553+
const dims = ctx.containerDimensions;
554+
if (dims && "height" in dims) root.style.height = `${dims.height}px`;
555+
if (dims && "width" in dims) root.style.width = `${dims.width}px`;
556+
});
557+
await app.connect();
558+
```
539559

540560
> [!WARNING]
541561
> Do not combine `autoResize: true` with `height: 100vh` or `100%` on the root element. The SDK reports the document height, the host grows the iframe to match, the document sees a taller viewport and grows again. This loops until the host's maximum height cap.
542562
543-
The React `useApp` hook always creates the App with `autoResize: true`. For fixed or host-driven height, construct the `App` manually or use the `useAutoResize` hook with a specific element.
563+
The React `useApp` hook always creates the App with `autoResize: true` and does not currently expose an option to disable it. For fixed or host-driven height, construct the `App` manually with `{ autoResize: false }` instead of using `useApp`.
544564

545565
## Passing contextual information from the App to the model
546566

@@ -714,7 +734,10 @@ return {
714734

715735
```ts
716736
// In the App, branch on the discriminator
717-
app.ontoolresult = (result) => {
737+
app.addEventListener("toolresult", (result) => {
738+
if (result.isError || !result.structuredContent) {
739+
return renderError(result);
740+
}
718741
const data = result.structuredContent as { kind: string };
719742
switch (data.kind) {
720743
case "open-document":
@@ -724,7 +747,7 @@ app.ontoolresult = (result) => {
724747
renderSearchResults(data);
725748
break;
726749
}
727-
};
750+
});
728751
```
729752

730753
## Conditionally showing UI
@@ -779,12 +802,16 @@ Use {@link app!App.openLink `app.openLink()`} instead of `window.open()` or `<a
779802
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:
780803

781804
```ts
782-
if (app.getHostCapabilities()?.openLinks) {
783-
await app.openLink({ url: "https://example.com/docs" });
784-
}
805+
const caps = app.getHostCapabilities();
806+
807+
// Hide controls the host cannot honor
808+
docsLink.hidden = !caps?.openLinks;
809+
downloadButton.hidden = !caps?.downloadFile;
785810

786-
if (app.getHostCapabilities()?.downloadFile) {
787-
await app.downloadFile({
811+
docsLink.onclick = () => app.openLink({ url: "https://example.com/docs" });
812+
813+
downloadButton.onclick = () =>
814+
app.downloadFile({
788815
contents: [
789816
{
790817
type: "resource",
@@ -796,7 +823,6 @@ if (app.getHostCapabilities()?.downloadFile) {
796823
},
797824
],
798825
});
799-
}
800826
```
801827

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

docs/troubleshooting.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ description: Diagnose common MCP App issues including blank iframes, CSP errors,
88

99
## Blank iframe
1010

11+
> [!TIP]
12+
> The fastest way to diagnose any of the issues below is to load your App in the reference host, which logs all protocol traffic to the browser console. See [Test with basic-host](./testing-mcp-apps.md#test-with-basic-host).
13+
1114
The most common causes, in the order you should check them:
1215

1316
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.
@@ -20,9 +23,9 @@ The most common causes, in the order you should check them:
2023

2124
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.
2225

23-
## `ontoolinput` / `ontoolresult` never fires
26+
## `toolinput` / `toolresult` events never fire
2427

25-
- **Handlers registered too late.** Attach `app.ontoolresult` before calling `connect()`. If the handler is attached after `connect()` resolves, the notification may have already been delivered and discarded. The React `useApp` hook handles this ordering automatically.
28+
- **Listeners registered too late.** Call `app.addEventListener("toolresult", …)` before calling `connect()`. If the listener is attached after `connect()` resolves, the notification may have already been delivered and discarded. The React `useApp` hook handles this ordering automatically.
2629
- **Tool was not called.** If the model chose a different tool, or none, there is no result to deliver. Check the host's tool-call log.
2730
- **SDK version mismatch.** Older SDK versions used stricter schemas for host notifications. If the App was built against a significantly older `@modelcontextprotocol/ext-apps` than the host expects, the initialize handshake can fail silently. Keep the SDK version current.
2831

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)