Skip to content

feat: add appCapabilities.preferredDisplayMode - #845

Draft
ochafik wants to merge 8 commits into
mainfrom
ochafik/preferred-display-mode
Draft

ochafik wants to merge 8 commits into
mainfrom
ochafik/preferred-display-mode

Conversation

@ochafik

@ochafik ochafik commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Note

Draft proposal, opened for discussion. The open questions at the end are real ones.

Problem

A View that is mainly useful on a large surface (an editor, a dashboard, a map) cannot say so. It starts in the Host's default display mode, usually inline, then has to send ui/request-display-mode and be moved, which the user sees as a jump.

Prior art and credit

The field comes from OpenAI's public MCP extensions specification. Credit to its authors. This PR takes the name and the meaning from there, and differs in where the field lives:

OpenAI MCP extensions This PR
Field _meta["openai/ui"].preferredDisplayMode, "Preferred initial display mode" appCapabilities.preferredDisplayMode, same meaning
Where On the UI resource content item, next to an availableDisplayModes there In ui/initialize, beside the View's own availableDisplayModes
Values "inline" | "fullscreen" The three display modes of this spec
Effect The host "considers preferredDisplayMode when choosing the initial display mode" A hint that the Host weighs when it first shows the View

Their stated reason for putting it on the resource is that appCapabilities.availableDisplayModes is known only after the app has initialized, which takes time; "Alternatives considered" below discusses that placement. The spec section carries a note that points to their document.

Proposal

This change adds one optional field, appCapabilities.preferredDisplayMode, sent in ui/initialize beside availableDisplayModes: the display mode in which the View prefers to be first shown. No new message, no new display mode. Draft spec only (specification/draft/apps.mdx).

  • It is a hint. The Host chooses how it first shows the View and MAY ignore the preference. It MUST NOT refuse ui/initialize because it does not offer the preferred mode, or because the View does not list it.
  • A Host that applies it SHOULD do so before it first shows the View, and SHOULD report the result in hostContext.displayMode.
  • A Host that has already shown the View (for example inline, while the tool runs) MAY instead apply it once, no later than when it sends the first ui/notifications/tool-result, unless the user has changed the View's display mode in the meantime. After that, it SHOULD NOT use the preference to change the mode without a user action.
  • The View MUST also list the mode in availableDisplayModes, and MUST handle being shown in another mode.

Schema change

 interface McpUiAppCapabilities {
   // ...
   availableDisplayModes?: Array<"inline" | "fullscreen" | "pip">;
+  /**
+   * Display mode in which the app prefers to be first shown. A hint: the host MAY ignore it.
+   * See Display Modes section for details.
+   * @example "fullscreen"
+   */
+  preferredDisplayMode?: "inline" | "fullscreen" | "pip";
 }

What is in the diff

  • Spec (draft): a "Preferred Display Mode" subsection, seven requirement lines, a security note.
  • SDK: the type in src/spec.types.ts and the regenerated schemas. No new API: a View declares the preference in the App capabilities, and a Host reads it with AppBridge.getAppCapabilities() once the View has initialized. Both get a type-checked JSDoc example.
  • Tests: the bridge receives the field; a View learns from host context that the preference was applied; an unknown value is refused.
  • Reference host (examples/basic-host): applies a fullscreen preference once, when the View has initialized and before it sends tool input, and only if the View also lists fullscreen in availableDisplayModes. Fullscreen now shows a small host-owned bar that names the app and offers "Exit fullscreen", since a View can now start there before the user has seen it.
  • Docs: the fullscreen section of docs/patterns.md, and a row in docs/migrate_from_openai_apps.md.

Example

// View -> Host
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "ui/initialize",
  "params": {
    "protocolVersion": "2026-01-26",
    "appInfo": { "name": "example-view", "version": "1.0.0" },
    "appCapabilities": {
      "availableDisplayModes": ["inline", "fullscreen"],
      "preferredDisplayMode": "fullscreen"
    }
  }
}

// Host -> View. Either: a Host that reads the request before it answers applies the preference in the result
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2026-01-26",
    "hostInfo": { "name": "example-host", "version": "1.0.0" },
    "hostCapabilities": {},
    "hostContext": {
      "displayMode": "fullscreen",
      "availableDisplayModes": ["inline", "fullscreen"]
    }
  }
}

// Or: a Host that has already answered sends this after ui/notifications/initialized.
// The SDK's AppBridge answers ui/initialize itself, so an SDK Host takes this path
// (setHostContext or sendHostContextChange); basic-host does.
{
  "jsonrpc": "2.0",
  "method": "ui/notifications/host-context-changed",
  "params": { "displayMode": "fullscreen" }
}

Compatibility

The field is optional: nothing changes for Views that do not send it. When a View does send it:

  • The SDK's AppBridge, on main before this change and in the published 1.7.5 (the ext-apps-v1 alias that src/interop-v1.test.ts runs), accepts the request and strips the field (checked by hand: this branch's App against the 1.7.5 AppBridge in memory, and main's generated schema; not a test in this diff).
  • A Host that validates against the published schema.json (additionalProperties: false) refuses the whole ui/initialize until it updates, as for every field added to appCapabilities. A View cannot detect this in advance.
  • A value outside "inline" | "fullscreen" | "pip" does not validate: after this change AppBridge refuses that ui/initialize (-32602, covered by a test), as it already does for such a value in availableDisplayModes. A new display mode would widen both fields at once.

Security considerations

  • A View that starts in fullscreen or pip (a concern raised in Display mode negotiation is missing #41) gets a large or floating surface before the user has seen it, which can help it imitate Host UI. By the proposed text, a Host may decline such a start without a user action, and otherwise should keep its own controls visible.
  • No new capability: a Host could already grant the mode on ui/request-display-mode.
  • The SHOULD NOT above is there so that a View cannot use the preference to return to a mode that the user has left. For the same reason, a repeated ui/initialize from a View that is already shown does not renew it.

Alternatives considered

  • Only the UI resource's _meta.ui (where the prior art puts it). Known before the View loads, which suits Hosts that lay out at tool-call time. But the Host would read it before it knows the View's appCapabilities.availableDisplayModes, so it could not check the preference against that list unless the list moved to the resource too.

Open questions

  1. Should the UI resource's _meta.ui also carry the preference? If so, which wins?
  2. Should AppBridge gain a hook that sets the initial host context from appCapabilities, so that an SDK Host can answer in the ui/initialize result instead of a follow-up notification?
  3. Is "once, no later than the first ui/notifications/tool-result" the right bound for a Host that has already shown the View? It allows a Host that renders the View inline while the tool runs to move it when the result lands; a stricter text would be "before it first shows the View" only.

Related

#41 (display mode negotiation), #152 (ui/request-display-mode), #739 (proposes another display mode and edits the same spec section).

AI use

Drafted with AI assistance (Claude). Opened as a Draft: I have not finished reviewing every line yet, and will before marking it ready.


Generated by Claude Code

claude and others added 5 commits November 4, 2026 01:05
A View can name, in ui/initialize, the display mode in which it prefers
to be first shown. The preference is a hint: the Host may ignore it, and
after the View's first showing does not use it to move the View without
a user action.

Co-authored-by: ochafik <ochafik@anthropic.com>
Add the type, regenerate the schemas, and cover it in the App/AppBridge
tests: the bridge receives the field, the View learns from host context
that it was applied, and an unknown value is refused. JSDoc examples
show how a View declares the preference and how a host reads it from
getAppCapabilities().

Co-authored-by: ochafik <ochafik@anthropic.com>
Move a View that prefers fullscreen, and lists it as available, to
fullscreen once it has initialized. Fullscreen now shows a host-owned
bar with the app's name and an exit button, since a View can start
there before the user has seen it.

Co-authored-by: ochafik <ochafik@anthropic.com>
Co-authored-by: ochafik <ochafik@anthropic.com>
Mention it in the fullscreen pattern and in the OpenAI migration guide.

Co-authored-by: ochafik <ochafik@anthropic.com>
Comment thread src/app-bridge.examples.ts Resolved
Comment thread src/app.examples.ts Resolved
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

📖 Docs Preview Deployed

Preview (stable) https://pr-845.mcp-ext-apps-docs-preview.pages.dev
This commit https://4767130a.mcp-ext-apps-docs-preview.pages.dev
Commit e23a2f6

Includes drafts and future-dated posts. All pages served with noindex, nofollow — search engines will not crawl this preview.

@pkg-pr-new

pkg-pr-new Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/@modelcontextprotocol/ext-apps@845

@modelcontextprotocol/server-basic-preact

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-preact@845

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-react@845

@modelcontextprotocol/server-basic-solid

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-solid@845

@modelcontextprotocol/server-basic-svelte

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-svelte@845

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vanillajs@845

@modelcontextprotocol/server-basic-vue

npm i https://pkg.pr.new/@modelcontextprotocol/server-basic-vue@845

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/@modelcontextprotocol/server-budget-allocator@845

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/@modelcontextprotocol/server-cohort-heatmap@845

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/@modelcontextprotocol/server-customer-segmentation@845

@modelcontextprotocol/server-debug

npm i https://pkg.pr.new/@modelcontextprotocol/server-debug@845

@modelcontextprotocol/server-lazy-auth

npm i https://pkg.pr.new/@modelcontextprotocol/server-lazy-auth@845

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/@modelcontextprotocol/server-map@845

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/@modelcontextprotocol/server-pdf@845

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/@modelcontextprotocol/server-scenario-modeler@845

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/@modelcontextprotocol/server-shadertoy@845

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/@modelcontextprotocol/server-sheet-music@845

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/@modelcontextprotocol/server-system-monitor@845

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/@modelcontextprotocol/server-threejs@845

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/@modelcontextprotocol/server-transcript@845

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/@modelcontextprotocol/server-video-resource@845

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/@modelcontextprotocol/server-wiki-explorer@845

commit: c3c0a99

Co-authored-by: ochafik <ochafik@anthropic.com>

ochafik commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

cc @fbauer33 @idosal


Generated by Claude Code

claude and others added 2 commits November 4, 2026 03:06
Co-authored-by: ochafik <ochafik@anthropic.com>
This reverts commit 7a724a0.

Co-authored-by: ochafik <ochafik@anthropic.com>

ochafik commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

On the "unused function" and "unused variable" findings from the code-quality bot: I am leaving these as they are. They are all in the type-checked example files (*.examples.ts(x), docs/patterns.tsx), where each function exists only so that its //#region can be copied into JSDoc and the docs. None of the existing example functions in those files is called or exported either, so changing only the new ones would make them the odd ones out.


Generated by Claude Code

@fbauer33

fbauer33 commented Nov 4, 2026

Copy link
Copy Markdown

thank you for the proposal! We learned from our users that changing from one view to another during render sometimes feels a bit jumpy, so our proposal is to put the available and preferred modes on the resource so we know before render where to display.

@idosal

idosal commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

@ochafik let's move preferred to _meta

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants