Skip to content

[Schema][Server] Add MCP Apps extension (io.modelcontextprotocol/ui) support#281

Open
chr-hertel wants to merge 1 commit into
mainfrom
feat/mcp-apps-extension
Open

[Schema][Server] Add MCP Apps extension (io.modelcontextprotocol/ui) support#281
chr-hertel wants to merge 1 commit into
mainfrom
feat/mcp-apps-extension

Conversation

@chr-hertel
Copy link
Copy Markdown
Member

@chr-hertel chr-hertel commented Apr 11, 2026

Summary

  • Add typed schema classes under Mcp\Schema\Extension\Apps\ for the MCP Apps extension (io.modelcontextprotocol/ui, spec 2026-01-26)
  • Add extensions field to both ServerCapabilities and ClientCapabilities for extension negotiation
  • Add ToolVisibility enum (model/app)
  • Add weather dashboard example in examples/server/mcp-apps/ demonstrating UI resources and tool-UI linkage
  • Add 34 new tests covering all schema classes, serialization, and capabilities backward compatibility

Details

The MCP Apps extension enables servers to expose interactive HTML UI applications as MCP resources. This PR adds server-side support:

  • Schema classes: McpApps (constants/helpers), UiResourceCsp, UiResourcePermissions, UiResourceContentMeta, UiToolMeta — all with fromArray()/jsonSerialize() and toMetaArray() helpers for embedding in _meta
  • Capabilities: extensions parameter on both capability classes (backward-compatible, defaults to null)
  • No new JSON-RPC methods — the extension uses existing resources/list, resources/read, tools/list, tools/call
  • Out of scope: View-Host protocol (ui/initialize, ui/open-link, etc.) which runs between the rendered HTML and the host application

Test plan

  • make cs — coding standards pass
  • make phpstan — static analysis passes (0 errors)
  • make unit-tests — all 754 tests pass (34 new)
  • Manual: npx @modelcontextprotocol/inspector php examples/server/mcp-apps/server.php

Running in Goose

Screencast.from.2026-05-11.23-45-55.webm

@chr-hertel chr-hertel force-pushed the feat/mcp-apps-extension branch from 9c9b41b to d9ca7e6 Compare April 13, 2026 21:47
@chr-hertel chr-hertel added Server Issues & PRs related to the Server component Schema Issues & PRs related to the Schema component and removed Server Issues & PRs related to the Server component labels Apr 20, 2026
@chr-hertel chr-hertel changed the title feat: add MCP Apps extension (io.modelcontextprotocol/ui) support [Schema] Add MCP Apps extension (io.modelcontextprotocol/ui) support Apr 20, 2026
@chr-hertel chr-hertel added improves spec compliance Improves consistency with other SDKs such as TyepScript needs more work Not ready to be merged yet, needs additional follow-up from the author(s). labels Apr 20, 2026
@chr-hertel chr-hertel force-pushed the feat/mcp-apps-extension branch from d9ca7e6 to ea82312 Compare April 20, 2026 19:24
@chr-hertel chr-hertel added this to the 0.6.0 milestone Apr 26, 2026
@chr-hertel chr-hertel force-pushed the feat/mcp-apps-extension branch 8 times, most recently from 1f1c1c6 to f1a76a6 Compare May 11, 2026 21:38
@chr-hertel chr-hertel added the Server Issues & PRs related to the Server component label May 11, 2026
@chr-hertel chr-hertel changed the title [Schema] Add MCP Apps extension (io.modelcontextprotocol/ui) support [Schema][Server] Add MCP Apps extension (io.modelcontextprotocol/ui) support May 11, 2026
@chr-hertel chr-hertel force-pushed the feat/mcp-apps-extension branch 2 times, most recently from 2039755 to cf9ee8e Compare May 11, 2026 22:06
@chr-hertel chr-hertel marked this pull request as ready for review May 11, 2026 22:07
@chr-hertel
Copy link
Copy Markdown
Member Author

This could use some love as follow up when it comes to the Builder. maybe a addApp() method would be nice, but didn't want to bloat the PR even more.

@chr-hertel chr-hertel removed the needs more work Not ready to be merged yet, needs additional follow-up from the author(s). label May 11, 2026
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
@chr-hertel chr-hertel force-pushed the feat/mcp-apps-extension branch from cf9ee8e to fda2228 Compare May 18, 2026 18:44
Comment on lines +42 to +52
* @param UiResourceContentMetaData $data
*/
public static function fromArray(array $data): self
{
return new self(
csp: isset($data['csp']) ? UiResourceCsp::fromArray($data['csp']) : null,
permissions: isset($data['permissions']) ? UiResourcePermissions::fromArray($data['permissions']) : null,
domain: $data['domain'] ?? null,
prefersBorder: $data['prefersBorder'] ?? null,
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 riskjsonSerialize() return type annotation UiResourceContentMetaData claims csp?: UiResourceCspData (array shape) but the property stores a UiResourceCsp object. PHPStan-level lie; serialization still works via recursive JsonSerializable.

Fix: annotate as array{csp?: UiResourceCsp, permissions?: UiResourcePermissions, domain?: string, prefersBorder?: bool}, or call ->jsonSerialize() explicitly on each.

*
* @author Christopher Hertel <mail@christopher-hertel.de>
*/
final class UiResourcePermissions implements \JsonSerializable
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk — Permission set hardcoded to 4 entries (camera/microphone/geolocation/clipboardWrite). If the spec adds new permissions, fromArray() silently drops them and jsonSerialize() cannot emit them.

Fix: keep unknown keys as passthrough (array<string, \stdClass>) or document the closed set explicitly.

Comment on lines +42 to +50
public static function fromArray(array $data): self
{
return new self(
camera: \array_key_exists('camera', $data),
microphone: \array_key_exists('microphone', $data),
geolocation: \array_key_exists('geolocation', $data),
clipboardWrite: \array_key_exists('clipboardWrite', $data),
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nit\array_key_exists('camera', $data) treats 'camera' => null as requested. Spec payload sends 'camera' => {}.

Fix: tighten to isset(), or accept either form and document the chosen contract.

*/
public function __construct(
public readonly ?string $resourceUri = null,
public readonly ?array $visibility = null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 risk?string[] $visibility forces callers to pass [ToolVisibility::Model->value, ...]. Verbose, typo-prone, defeats the existing ToolVisibility enum.

Fix: accept array<ToolVisibility>|array<string> and normalize in the constructor — catches typos at construction time.

final class McpApps implements ServerExtensionInterface
{
public const EXTENSION_ID = 'io.modelcontextprotocol/ui';
public const MIME_TYPE = 'text/html;profile=mcp-app';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question — MIME constant text/html;profile=mcp-app. RFC 6838 typically quotes the profile parameter (profile="mcp-app"). Is the unquoted form spec-mandated by io.modelcontextprotocol/ui, or just copied loosely?

'weather-app',
description: 'Interactive weather dashboard',
mimeType: McpApps::MIME_TYPE,
meta: ['ui' => new stdClass()],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nitmeta: ['ui' => new stdClass()] is a magic presence marker. Future readers will not know why an empty stdClass is required vs. just omitting the key.

Fix: introduce a typed marker (e.g. McpApps::resourceMarker(): \stdClass) so intent is discoverable and the empty-object protocol detail is hidden behind a named API.

let initialized = false;
const pending = new Map();

function post(msg) { window.parent.postMessage(msg, '*'); }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nitpost(msg) uses '*' as postMessage target origin. That's a spec-level limitation (the iframe doesn't know its parent origin upfront), but worth an inline comment here — the receiving host must validate sender origin on its side.

clientInfo: { name: 'weather-app', version: '1.0.0' },
});
} catch { /* degrade gracefully on non-conforming hosts */ }
initialized = true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nitinitialized = true is set but never read anywhere in the file. Dead variable.

Comment thread docs/extensions.md
),
'ui://my-app',
mimeType: McpApps::MIME_TYPE,
meta: ['ui' => new \stdClass()],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nit — The example uses meta: ['ui' => new \stdClass()] on the resource and meta: ['ui' => new UiResourceContentMeta(...)] inside TextResourceContents a few lines up. Two different _meta.ui shapes (presence marker vs. structured metadata) — readers will conflate them.

Fix: add one sentence clarifying when each shape applies.

Comment on lines +65 to +77
if (null !== $this->connectDomains) {
$data['connectDomains'] = $this->connectDomains;
}
if (null !== $this->resourceDomains) {
$data['resourceDomains'] = $this->resourceDomains;
}
if (null !== $this->frameDomains) {
$data['frameDomains'] = $this->frameDomains;
}
if (null !== $this->baseUriDomains) {
$data['baseUriDomains'] = $this->baseUriDomains;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 nit — Empty array [] for any of the *Domains fields serializes to "connectDomains": [] etc. Spec question: is an empty list "allow none" or "omit field"? If "omit", change the guards to if ($this->connectDomains) (truthy check) instead of null !== ....

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

Labels

improves spec compliance Improves consistency with other SDKs such as TyepScript Schema Issues & PRs related to the Schema component Server Issues & PRs related to the Server component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants