php-sdk/docs/extensions.md
Christopher Hertel d150772e24
[Schema][Server] Add MCP Apps extension (io.modelcontextprotocol/ui) support (#281)
* feat: add MCP Apps extension (io.modelcontextprotocol/ui) support

Adds support for the MCP Apps protocol extension, which lets servers
expose interactive HTML UIs as resources rendered by spec-conforming
clients (e.g. Goose) in sandboxed iframes.

- Generic extension plumbing: ServerExtensionInterface and
  Builder::enableExtension(), with extensions advertised under
  capabilities.extensions during the initialize handshake.
  ClientCapabilities/ServerCapabilities gain an extensions field.
- Schema DTOs under Mcp\Schema\Extension\Apps: the McpApps marker,
  UiToolMeta, ToolVisibility enum, UiResourceContentMeta,
  UiResourceCsp, and UiResourcePermissions (with empty-object
  presence markers per spec).
- A worked weather example under examples/server/mcp-apps/ with a
  minimal spec-conforming view (ui/initialize handshake, postMessage
  JSONRPCMessage objects, and ResizeObserver-driven size reporting).
- docs/extensions.md guide with pointers from index.md and
  examples.md to the ext-apps repository for the TypeScript SDK and
  richer view-side patterns.
- Unit and inspector snapshot test coverage.

Spec: https://github.com/modelcontextprotocol/ext-apps

* Type UiToolMeta visibility as ToolVisibility enum

Also fix UiResourceContentMeta::jsonSerialize() return phpdoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Address review nitpicks for MCP Apps extension

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Apply PR review feedback for MCP Apps extension

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add regression tests and CHANGELOG entry for capabilities extensions

- Pin the is_array() guards on extensions in ServerCapabilities and
  ClientCapabilities::fromArray() with explicit non-array payload tests.
- Pin the isset()-rejects-null contract in UiResourcePermissions::fromArray().
- Surface the new extensions field on both capability classes in CHANGELOG.
- Inline the ?: idiom in Builder::build() to match the surrounding style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 09:35:54 +01:00

4.3 KiB

Protocol Extensions

MCP protocol extensions advertise additional, optional capabilities during the initialize handshake. A server opts in via Builder::enableExtension():

use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Server;

$server = Server::builder()
    ->setServerInfo('My Server', '1.0.0')
    ->enableExtension(new McpApps())
    ->build();

Pass one or more ServerExtensionInterface instances; multiple extensions can be enabled in a single call. Enabling the same extension twice throws a LogicException.

Note: extensions enabled via enableExtension() are merged into the extensions capability even when you supply your own ServerCapabilities via setCapabilities(). An enabled extension overrides any entry under the same id already present in those capabilities.

MCP Apps (io.modelcontextprotocol/ui)

The MCP Apps extension lets servers expose interactive HTML UIs as resources. Clients that support it render them in sandboxed iframes and bridge tool calls between the iframe (the View) and the server via the host.

A UI consists of two pieces wired together by _meta.ui:

  1. A resource with URI scheme ui:// and MIME type text/html;profile=mcp-app, returning the HTML body.
  2. A tool linked to that resource via UiToolMeta, so the client knows to open the UI when the tool is invoked.
use Mcp\Schema\Content\TextResourceContents;
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Schema\Extension\Apps\ToolVisibility;
use Mcp\Schema\Extension\Apps\UiResourceContentMeta;
use Mcp\Schema\Extension\Apps\UiResourceCsp;
use Mcp\Schema\Extension\Apps\UiResourcePermissions;
use Mcp\Schema\Extension\Apps\UiToolMeta;

$server = Server::builder()
    ->enableExtension(new McpApps())
    ->addResource(
        fn () => new TextResourceContents(
            uri: 'ui://my-app',
            mimeType: McpApps::MIME_TYPE,
            text: file_get_contents(__DIR__.'/app.html'),
            meta: ['ui' => new UiResourceContentMeta(
                csp: new UiResourceCsp(connectDomains: ['https://api.example.com']),
                permissions: new UiResourcePermissions(geolocation: true),
                prefersBorder: true,
            )],
        ),
        'ui://my-app',
        mimeType: McpApps::MIME_TYPE,
        meta: ['ui' => McpApps::resourceMarker()],
    )
    ->addTool(
        $myToolHandler,
        'my_tool',
        meta: ['ui' => new UiToolMeta(
            resourceUri: 'ui://my-app',
            visibility: [ToolVisibility::Model, ToolVisibility::App],
        )],
    )
    ->build();

Note the two distinct _meta.ui shapes: the resource descriptor (its resources/list entry) carries only an empty marker — McpApps::resourceMarker() — flagging it as an MCP App, while the resource content returned by resources/read carries the structured UiResourceContentMeta with the actual CSP and permission configuration.

Server-side DTOs

Class Purpose
McpApps Extension marker; provides EXTENSION_ID, MIME_TYPE, URI_SCHEME constants.
UiToolMeta Tool _meta.ui payload: resourceUri + visibility.
ToolVisibility Enum: Model, App.
UiResourceContentMeta Resource content _meta.ui: csp, permissions, domain, prefersBorder.
UiResourceCsp CSP allow-lists: connectDomains, resourceDomains, frameDomains, baseUriDomains.
UiResourcePermissions Sandbox permissions: camera, microphone, geolocation, clipboardWrite.

Writing the HTML view

The View and host exchange JSONRPCMessage objects (not JSON strings) via window.parent.postMessage. Before the host forwards tools/call, tool-input, or tool-result, the View must complete the spec-mandated handshake:

  1. View → Host: ui/initialize request
  2. Host → View: response with hostCapabilities, hostInfo, hostContext
  3. View → Host: ui/notifications/initialized
  4. View → Host: ui/notifications/size-changed whenever the iframe wants to resize

See the ext-apps repository for the full protocol, official TypeScript SDK (@modelcontextprotocol/ext-apps), and view-side examples. A working minimal view is included in examples/server/mcp-apps/weather-app.html.