diff --git a/docs/design-guidelines.md b/docs/design-guidelines.md new file mode 100644 index 00000000..4d416416 --- /dev/null +++ b/docs/design-guidelines.md @@ -0,0 +1,59 @@ +--- +title: Design Guidelines +group: Getting Started +description: UX guidance for MCP Apps, covering host-provided chrome, content sizing, and visual consistency with the surrounding chat. +--- + +# Design Guidelines + +An MCP App is part of a conversation. It should read as a continuation of the chat, not as a separate application embedded inside it. + +## Host chrome + +Hosts render a frame around your App that typically includes: + +- A title bar showing the App name (from tool or server metadata) +- Display-mode controls (expand, collapse, close) +- Attribution indicating which connector or server provided the App + +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. + +A title inside the content area (for example, "Q3 Revenue by Region" above a chart) is acceptable. The App's brand name is not. + +## Scope + +An MCP App answers one question or supports one task. Avoid building a full dashboard with tabs, sidebars, and settings panels. + +- 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. +- Limit inline mode to one primary action. A "Confirm" button is appropriate; a toolbar with eight icons is not. +- 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. + +## Host UI imitation + +Your App must not resemble the surrounding chat client. Do not render: + +- Chat bubbles or message threads +- Anything that resembles the host's text input or send button +- System notifications or permission dialogs + +These patterns blur the line between host UI and App content, and most hosts prohibit them in their submission guidelines. + +## Host styling + +Hosts provide CSS custom properties for colors, fonts, spacing, and border radius (see [Adapting to host context](./patterns.md#adapting-to-host-context-theme-styling-fonts-and-safe-areas)). Using them keeps your App consistent across light mode, dark mode, and different host themes. + +Brand colors are appropriate for content elements such as chart series or status badges. Backgrounds, text, and borders should use host variables. Always provide fallback values so the App renders correctly on hosts that omit some variables. + +## Display modes + +Design for inline mode first. It is the default, and it is narrow (often the width of a chat message) and height-constrained. + +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. + +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. + +## Loading and empty states + +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. + +If the tool result can be empty (no search results, empty cart), design an explicit empty state rather than rendering nothing. diff --git a/docs/overview.md b/docs/overview.md index 28f5b237..67f70575 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -110,6 +110,8 @@ Resources are declared upfront, during tool registration. This design enables: - **Separation of concerns** — Templates (presentation) are separate from tool results (data) - **Review** — Hosts can inspect UI templates during connection setup +**Versioning and caching.** Resource caching behavior is host-defined. A host may re-fetch the `ui://` resource on each render, cache it for the session, or persist it alongside the conversation. When a user revisits an old conversation, the host may run the current template code against the original tool result, or it may replay a snapshot of both from the time of the original tool call. Design the App to tolerate older `structuredContent` shapes: handle unknown fields gracefully and do not assume the template and the data were produced by the same code version. + See the [UI Resource Format](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx#ui-resource-format) section of the specification for the full schema. ## Tool-UI Linkage diff --git a/docs/patterns.md b/docs/patterns.md index 87a6d823..beb7da68 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -37,6 +37,35 @@ registerAppTool( > [!NOTE] > For full examples that implement this pattern, see: [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) and [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server). +## Tool result data visibility + +A tool result has three fields for data, each with different visibility: + +| Field | Seen by model | Seen by App | Use for | +| ------------------- | ------------- | ----------- | ------------------------------------------------------------- | +| `content` | Yes | Yes | Short text summary for the model and for text-only hosts | +| `structuredContent` | No | Yes | Structured data the App renders (tables, charts, lists) | +| `_meta` | No | Yes | Opaque metadata such as IDs, timestamps, and view identifiers | + +Keep `content` brief. The model uses it to decide what to say next, so a one-line summary is preferable to raw data. + +> [!WARNING] +> 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. + +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: + +```ts +return { + content: [ + { + type: "text", + text: "Rendered an interactive chart of Q3 revenue by region. The user can see and interact with it; do not describe the chart contents in your response.", + }, + ], + structuredContent: { regions, revenue, quarter: "Q3" }, +}; +``` + ## Polling for live data For real-time dashboards or monitoring views, use an app-only tool (with `visibility: ["app"]`) that the App polls at regular intervals. @@ -402,6 +431,29 @@ function MyApp() { > [!NOTE] > For full examples that implement this pattern, see: [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) and [`examples/basic-server-react/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react). +> [!TIP] +> Avoid setting the `color-scheme` CSS property on your root element. If the App declares `color-scheme: light dark` and the host document does not, the browser inserts an opaque backdrop behind the iframe to prevent cross-scheme bleed-through, which breaks transparent backgrounds. Use the `[data-theme]` attribute approach shown above and let the host control scheme negotiation. + +## Supporting touch devices + +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: + +```css +/* Chart or canvas that handles its own panning */ +.chart-surface { + touch-action: none; +} + +/* Horizontal slider that should not trigger vertical page scroll */ +.slider-track { + touch-action: pan-y; /* allow vertical scroll, consume horizontal */ +} +``` + +Without `touch-action`, dragging across the App on a mobile device also scrolls the chat, and the App may never receive `pointermove` events. + +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. + ## Entering / exiting fullscreen Toggle fullscreen mode by calling {@link app!App.requestDisplayMode `requestDisplayMode`}: @@ -453,6 +505,39 @@ In fullscreen mode, remove the container's border radius so content extends to t > [!NOTE] > For full examples that implement this pattern, see: [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server), [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server), and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server). +## Controlling App height + +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. + +There are three height strategies: + +**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. + +**Fixed height.** For UI that should remain the same size when inline. Disable auto-resize and set an explicit height: + +```ts +const app = new App( + { name: "my-app", version: "0.1.0" }, + {}, + { autoResize: false }, +); +``` + +```css +html, +body { + height: 500px; + margin: 0; +} +``` + +**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`}. + +> [!WARNING] +> 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. + +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. + ## Passing contextual information from the App to the model Use {@link app!App.updateModelContext `updateModelContext`} to keep the model informed about what the user is viewing or interacting with. Structure the content with YAML frontmatter for easy parsing: @@ -569,6 +654,11 @@ app.ontoolresult = (result) => { For state that represents user effort (e.g., saved bookmarks, annotations, custom configurations), consider persisting it server-side using [app-only tools](#tools-that-are-private-to-apps) instead. Pass the `viewUUID` to the app-only tool to scope the saved data to that view instance. +> [!WARNING] +> Namespace all `localStorage` keys. Hosts typically serve every MCP App from the same sandbox origin, so all Apps share a single `localStorage`. Generic keys such as `"state"` or `"settings"` will collide with other Apps. The server-generated `viewUUID` pattern above avoids this; any additional keys should be prefixed with a string unique to your App. +> +> `localStorage` availability is host-dependent and may be disabled in some sandbox configurations. Wrap access in `try`/`catch` and degrade gracefully. + > [!NOTE] > For full examples using `localStorage`, see: [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) (persists current page) and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) (persists camera position). @@ -601,6 +691,61 @@ app.onteardown = async () => { > [!NOTE] > For full examples that implement this pattern, see: [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) and [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server). +## Sharing one UI resource across multiple tools + +Several tools can reference the same `ui://` resource. For example, a single document viewer App might render results from `open-document`, `search-documents`, and `recent-documents`. + +The App needs to know which tool produced its data in order to parse the payload correctly. The host may provide this via `hostContext.toolInfo`, but the field is optional and not available on every host. The reliable approach is to include a discriminator in the tool result: + +```ts +// In each tool handler, tag the result with its origin +return { + content: [{ type: "text", text: "Opened annual-report.pdf" }], + structuredContent: { + kind: "open-document", // discriminator + document: { id, title, pageCount }, + }, +}; +``` + +```ts +// In the App, branch on the discriminator +app.ontoolresult = (result) => { + const data = result.structuredContent as { kind: string }; + switch (data.kind) { + case "open-document": + renderViewer(data); + break; + case "search-documents": + renderSearchResults(data); + break; + } +}; +``` + +## Conditionally showing UI + +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. + +If both behaviors are needed, register two tools: + +- `query-data` with no `_meta.ui`, returning text and structured data for the model to reason about +- `visualize-data` with `_meta.ui`, returning the same data rendered as an interactive App + +Write distinct descriptions so the model selects the correct tool based on user intent ("show me" maps to visualize, "tell me" maps to query). + +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. + +## Opening external links + +Use {@link app!App.openLink `app.openLink()`} instead of `window.open()` or ``. The sandbox blocks direct navigation; `openLink` asks the host to open the URL on the App's behalf. + +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. + +```ts +await app.openLink({ url: "https://example.com/docs" }); +``` + ## Lowering perceived latency 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 `
` tag before executing it, partially rendering a table as data arrives, or incrementally populating a chart.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
new file mode 100644
index 00000000..515827b8
--- /dev/null
+++ b/docs/troubleshooting.md
@@ -0,0 +1,56 @@
+---
+title: Troubleshooting
+group: Getting Started
+description: Diagnose common MCP App issues including blank iframes, CSP errors, missing tool callbacks, and cross-host rendering differences.
+---
+
+# Troubleshooting
+
+## Blank iframe
+
+The most common causes, in the order you should check them:
+
+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.
+
+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).
+
+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.
+
+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.
+
+## `ontoolinput` / `ontoolresult` never fires
+
+- **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.
+- **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.
+- **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.
+
+## App works in one host but not another
+
+MCP Apps are portable only if they use the SDK exclusively. Common portability mistakes:
+
+- **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.
+- **Hardcoded CDN URLs.** Bundle assets into the App or declare their origins in `resourceDomains`.
+- **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).
+
+## App grows unbounded or has the wrong height
+
+See [Controlling App height](./patterns.md#controlling-app-height). The most common cause is `height: 100vh` combined with the default `autoResize: true`.
+
+## Network requests fail with CORS errors
+
+CSP and CORS are separate controls with different error messages and different fixes:
+
+- **CSP** (`Refused to connect`): The browser blocked the request because the domain is not in `connectDomains`. Add the domain to `_meta.ui.csp` on the MCP server.
+- **CORS** (`No 'Access-Control-Allow-Origin' header`): The API server rejected the request because it does not recognize the sandbox origin. Add the origin to the API server's allowlist, or use `_meta.ui.domain` to get a predictable origin that can be allowlisted.
+
+See the [CSP & CORS guide](./csp-cors.md) for configuration examples.
+
+## Opaque background instead of transparent
+
+If the App declares `color-scheme: light dark` (or `color-scheme: dark`) and the host document does not, browsers insert an opaque backdrop behind the iframe to prevent cross-scheme bleed-through. Remove the `color-scheme` declaration and use the `[data-theme]` attribute pattern from the [host context guide](./patterns.md#adapting-to-host-context-theme-styling-fonts-and-safe-areas).
+
+## Getting help
+
+- Test against the reference host: run `npm start` in this repository to serve `examples/basic-host` at `http://localhost:8080`. It logs all protocol traffic to the console.
+- Search [GitHub Discussions](https://github.com/modelcontextprotocol/ext-apps/discussions) for similar issues.
+- File a bug with a minimal reproduction in [GitHub Issues](https://github.com/modelcontextprotocol/ext-apps/issues).
diff --git a/typedoc.config.mjs b/typedoc.config.mjs
index a97bb29b..b8ae8872 100644
--- a/typedoc.config.mjs
+++ b/typedoc.config.mjs
@@ -14,6 +14,8 @@ const config = {
     "docs/agent-skills.md",
     "docs/testing-mcp-apps.md",
     "docs/patterns.md",
+    "docs/design-guidelines.md",
+    "docs/troubleshooting.md",
     "docs/authorization.md",
     "docs/csp-cors.md",
     "docs/migrate_from_openai_apps.md",