Apps¶
The MCP Apps extension (io.modelcontextprotocol/ui, SEP-1865) lets a tool link an interactive HTML view that
the host renders in a sandboxed iframe. The branding says "apps", but every protocol literal says ui. The
identifier is io.modelcontextprotocol/ui, the metadata rides the _meta key ui, and the view is an ordinary
MCP resource under the ui:// scheme with the text/html;profile=mcp-app mime type.
The extension ships in Nexus\Mcp\Extension\Apps. Like every extension, it is disabled until you enable it
explicitly:
use Nexus\Mcp\Extension\Apps\Schema\UiResourceCsp;
use Nexus\Mcp\Extension\Apps\Schema\UiResourceMeta;
use Nexus\Mcp\Extension\Apps\Schema\UiToolMeta;
use Nexus\Mcp\Extension\Apps\Server\AppsServerExtension;
use Nexus\Mcp\Extension\Apps\Server\UiResource;
use Nexus\Mcp\Server\ServerBuilder;
$dashboard = new UiResource(
name: 'dashboard',
uri: 'ui://weather-server/dashboard',
uiMeta: new UiResourceMeta(
csp: new UiResourceCsp(connectDomains: ['https://api.openweathermap.org']),
prefersBorder: true,
),
);
$server = (new ServerBuilder())
->setServerInfo('demo', '1.0.0')
->enableExtension(new AppsServerExtension())
->addResource($dashboard->resource, static fn(): string => file_get_contents(__DIR__.'/dashboard.html'))
->addTool($weatherTool, $weatherExecutor)
->build();
The extension defines no JSON-RPC methods, so enabling it only advertises the io.modelcontextprotocol/ui
capability slot. Everything else is metadata on the tools and resources the builder already registers.
Declaring a UI resource¶
The spec binds a UI resource to three MUSTs. The URI starts with ui://. The mime type is exactly
text/html;profile=mcp-app. The content is a valid HTML5 document.
UiResource composes a Resource that upholds the first two by construction. It attaches the optional
_meta.ui metadata, kept readable on $uiResource->uiMeta, and exposes the result on $uiResource->resource for
addResource().
The HTML validity of what the reader returns stays your responsibility. So does serving the same mime type and
metadata on the read contents, which reusing the composed Resource's fields holds by construction:
use Nexus\Mcp\Core\Schema\Resource\TextResourceContents;
use Nexus\Mcp\Extension\Apps\Apps;
$contents = new TextResourceContents(
uri: $dashboard->resource->uri,
text: $html,
mimeType: Apps::MIME_TYPE,
meta: $dashboard->resource->meta,
);
UiResourceMeta carries the sandbox configuration the host enforces: the CSP allow-lists (UiResourceCsp, where
an empty list means the same as an omitted one), the requested UiResourcePermissions (each encoded as a key
with an empty-object value), the host-defined dedicated domain, and the prefersBorder rendering hint. The spec
puts the same shape on both the resources/list descriptor and each resources/read content item, so declare it
in both places, as above.
Linking a tool to its view¶
A tool opts in through the _meta.ui object. Build it with UiToolMeta and attach it through the meta: slot of
manual or attribute registration:
use Nexus\Mcp\Extension\Apps\Schema\Enum\ToolVisibility;
#[AsTool(description: 'Returns the weather for a city.', meta: [
'ui' => [
'resourceUri' => 'ui://weather-server/dashboard',
'visibility' => ['model', 'app'],
],
])]
public function weather(string $city): string { /* ... */ }
UiToolMeta validates the ui:// scheme on resourceUri, and
(new UiToolMeta(resourceUri: ..., visibility: [ToolVisibility::App]))->toArray() produces the same array for the
manual path. An omitted visibility means the spec default ["model", "app"], and the SDK never materialises the
default on the envelope. The deprecated flat _meta["ui/resourceUri"] key is never emitted.
Capability direction¶
The negotiation is asymmetric. The client declares {"mimeTypes": ["text/html;profile=mcp-app"]} under the
extension slot. The spec prescribes no server-side settings, so AppsServerExtension advertises an empty object.
The spec's guidance that servers check client capabilities before they expose UI-enabled tools is per-request
under this revision. Read $context->meta->clientCapabilities->extensions['io.modelcontextprotocol/ui'] inside a
handler when you want to branch.
The SDK deliberately does not filter tools/list by that declaration. The tool metadata is inert for hosts that
ignore it, the spec asks tools to keep a text-only fallback anyway, and varying a cacheable listing per client
would fight the SEP-2549 cache semantics.
Everything under the ui/* postMessage family is the browser host's side of the extension: ui/initialize, the
host notifications, and the sandbox proxy. It never touches the MCP connection, so the SDK does not model it.
The client half is documented in Client apps, and examples/apps-e2e/ runs the whole flow, browser host included.