Skip to content

Commit fda2228

Browse files
committed
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
1 parent 3e45b1f commit fda2228

29 files changed

Lines changed: 1507 additions & 2 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to `mcp/sdk` will be documented in this file.
66
-----
77

88
* [BC Break] Bump default protocol version to `2025-11-25`
9+
* Add support for MCP Apps extension in schema and server
910
* Allow overriding the default name pattern for Discovery
1011
* Add configurable session garbage collection (`gcProbability`/`gcDivisor`)
1112
* Add `ChainLoader` to compose multiple `LoaderInterface` implementations via explicit ordering.

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"Mcp\\Example\\Server\\DiscoveryUserProfile\\": "examples/server/discovery-userprofile/",
7777
"Mcp\\Example\\Server\\EnvVariables\\": "examples/server/env-variables/",
7878
"Mcp\\Example\\Server\\ExplicitRegistration\\": "examples/server/explicit-registration/",
79+
"Mcp\\Example\\Server\\McpApps\\": "examples/server/mcp-apps/",
7980
"Mcp\\Example\\Server\\OAuthKeycloak\\": "examples/server/oauth-keycloak/",
8081
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
8182
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",

docs/examples.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,18 @@ npx @modelcontextprotocol/inspector php examples/server/elicitation/server.php
355355
2. **confirm_action** - Simple boolean confirmation dialog
356356
3. **collect_feedback** - Rating and comments form with optional fields
357357

358+
### MCP Apps
359+
360+
**File**: `examples/server/mcp-apps/`
361+
362+
A weather app demonstrating the [MCP Apps extension](extensions.md): a `ui://`
363+
HTML resource is opened by an MCP App-aware client (e.g. Goose) and bridged to
364+
the `get_weather` tool. The bundled `weather-app.html` performs the
365+
`ui/initialize` handshake, reports its size via `ui/notifications/size-changed`,
366+
and calls back into the server. See the
367+
[ext-apps repo](https://github.com/modelcontextprotocol/ext-apps) for the
368+
TypeScript SDK and richer view-side patterns.
369+
358370
## Client Examples
359371

360372
### STDIO Discovery Calculator (Client)

docs/extensions.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Protocol Extensions
2+
3+
MCP protocol extensions advertise additional, optional capabilities during the initialize handshake.
4+
A server opts in via `Builder::enableExtension()`:
5+
6+
```php
7+
use Mcp\Schema\Extension\Apps\McpApps;
8+
use Mcp\Server;
9+
10+
$server = Server::builder()
11+
->setServerInfo('My Server', '1.0.0')
12+
->enableExtension(McpApps::class) // or pre-built instances
13+
->build();
14+
```
15+
16+
Pass either a class string (the extension is instantiated with no arguments) or
17+
a pre-built `ServerExtensionInterface` instance. Multiple extensions can be
18+
enabled in a single call.
19+
20+
> Note: calling `setCapabilities()` overrides automatic capability detection,
21+
> so it also overrides the `extensions` field. If you set your own
22+
> `ServerCapabilities`, include the extensions you want yourself.
23+
24+
## MCP Apps (`io.modelcontextprotocol/ui`)
25+
26+
The [MCP Apps extension][ext-apps] lets servers expose interactive HTML UIs as
27+
resources. Clients that support it render them in sandboxed iframes and bridge
28+
tool calls between the iframe (the *View*) and the server via the host.
29+
30+
A UI consists of two pieces wired together by `_meta.ui`:
31+
32+
1. **A resource** with URI scheme `ui://` and MIME type
33+
`text/html;profile=mcp-app`, returning the HTML body.
34+
2. **A tool** linked to that resource via `UiToolMeta`, so the client knows to
35+
open the UI when the tool is invoked.
36+
37+
```php
38+
use Mcp\Schema\Content\TextResourceContents;
39+
use Mcp\Schema\Extension\Apps\McpApps;
40+
use Mcp\Schema\Extension\Apps\ToolVisibility;
41+
use Mcp\Schema\Extension\Apps\UiResourceContentMeta;
42+
use Mcp\Schema\Extension\Apps\UiResourceCsp;
43+
use Mcp\Schema\Extension\Apps\UiResourcePermissions;
44+
use Mcp\Schema\Extension\Apps\UiToolMeta;
45+
46+
$server = Server::builder()
47+
->enableExtension(McpApps::class)
48+
->addResource(
49+
fn () => new TextResourceContents(
50+
uri: 'ui://my-app',
51+
mimeType: McpApps::MIME_TYPE,
52+
text: file_get_contents(__DIR__.'/app.html'),
53+
meta: ['ui' => new UiResourceContentMeta(
54+
csp: new UiResourceCsp(connectDomains: ['https://api.example.com']),
55+
permissions: new UiResourcePermissions(geolocation: true),
56+
prefersBorder: true,
57+
)],
58+
),
59+
'ui://my-app',
60+
mimeType: McpApps::MIME_TYPE,
61+
meta: ['ui' => new \stdClass()],
62+
)
63+
->addTool(
64+
$myToolHandler,
65+
'my_tool',
66+
meta: ['ui' => new UiToolMeta(
67+
resourceUri: 'ui://my-app',
68+
visibility: [ToolVisibility::Model->value, ToolVisibility::App->value],
69+
)],
70+
)
71+
->build();
72+
```
73+
74+
### Server-side DTOs
75+
76+
| Class | Purpose |
77+
| --- | --- |
78+
| `McpApps` | Extension marker; provides `EXTENSION_ID`, `MIME_TYPE`, `URI_SCHEME` constants. |
79+
| `UiToolMeta` | Tool `_meta.ui` payload: `resourceUri` + `visibility`. |
80+
| `ToolVisibility` | Enum: `Model`, `App`. |
81+
| `UiResourceContentMeta` | Resource content `_meta.ui`: `csp`, `permissions`, `domain`, `prefersBorder`. |
82+
| `UiResourceCsp` | CSP allow-lists: `connectDomains`, `resourceDomains`, `frameDomains`, `baseUriDomains`. |
83+
| `UiResourcePermissions` | Sandbox permissions: `camera`, `microphone`, `geolocation`, `clipboardWrite`. |
84+
85+
### Writing the HTML view
86+
87+
The View and host exchange `JSONRPCMessage` **objects** (not JSON strings) via
88+
`window.parent.postMessage`. Before the host forwards `tools/call`,
89+
`tool-input`, or `tool-result`, the View must complete the spec-mandated
90+
handshake:
91+
92+
1. View → Host: `ui/initialize` request
93+
2. Host → View: response with `hostCapabilities`, `hostInfo`, `hostContext`
94+
3. View → Host: `ui/notifications/initialized`
95+
4. View → Host: `ui/notifications/size-changed` whenever the iframe wants to
96+
resize
97+
98+
See the [`ext-apps` repository][ext-apps] for the full protocol, official
99+
TypeScript SDK (`@modelcontextprotocol/ext-apps`), and view-side examples. A
100+
working minimal view is included in
101+
[`examples/server/mcp-apps/weather-app.html`](../examples/server/mcp-apps/weather-app.html).
102+
103+
[ext-apps]: https://github.com/modelcontextprotocol/ext-apps

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@
55
- [Client](client.md) — Client SDK for connecting to and communicating with MCP servers.
66
- [Transports](transports.md) — STDIO and HTTP transport implementations with guidance on choosing between them.
77
- [Server-Client Communication](server-client-communication.md) — Methods for servers to communicate back to clients: sampling, logging, progress, and notifications.
8+
- [Protocol Extensions](extensions.md) — Opt-in protocol extensions announced during capability negotiation, including MCP Apps (HTML UI resources).
89
- [Examples](examples.md) — Example projects demonstrating attribute-based discovery, dependency injection, HTTP transport, and more.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the official PHP MCP SDK.
5+
*
6+
* A collaboration between Symfony and the PHP Foundation.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Mcp\Example\Server\McpApps;
13+
14+
use Mcp\Schema\Content\TextResourceContents;
15+
use Mcp\Schema\Extension\Apps\McpApps;
16+
use Mcp\Schema\Extension\Apps\UiResourceContentMeta;
17+
use Mcp\Schema\Extension\Apps\UiResourceCsp;
18+
use Mcp\Schema\Extension\Apps\UiResourcePermissions;
19+
20+
final class WeatherApp
21+
{
22+
public function getWeatherApp(): TextResourceContents
23+
{
24+
$contentMeta = new UiResourceContentMeta(
25+
csp: new UiResourceCsp(
26+
connectDomains: ['https://api.weather.example.com'],
27+
),
28+
permissions: new UiResourcePermissions(
29+
geolocation: true,
30+
),
31+
prefersBorder: true,
32+
);
33+
34+
return new TextResourceContents(
35+
uri: 'ui://weather-app',
36+
mimeType: McpApps::MIME_TYPE,
37+
text: file_get_contents(__DIR__.'/weather-app.html'),
38+
meta: ['ui' => $contentMeta],
39+
);
40+
}
41+
42+
public function getWeather(string $city): string
43+
{
44+
$weather = [
45+
'london' => ['temp' => '15°C', 'condition' => 'Cloudy', 'humidity' => '78%'],
46+
'paris' => ['temp' => '18°C', 'condition' => 'Sunny', 'humidity' => '55%'],
47+
'tokyo' => ['temp' => '22°C', 'condition' => 'Partly Cloudy', 'humidity' => '65%'],
48+
'new york' => ['temp' => '12°C', 'condition' => 'Rainy', 'humidity' => '85%'],
49+
'lagos' => ['temp' => '30°C', 'condition' => 'Sunny', 'humidity' => '82%'],
50+
'stockholm' => ['temp' => '4°C', 'condition' => 'Cloudy', 'humidity' => '70%'],
51+
'berlin' => ['temp' => '9°C', 'condition' => 'Partly Cloudy', 'humidity' => '68%'],
52+
'sydney' => ['temp' => '26°C', 'condition' => 'Sunny', 'humidity' => '60%'],
53+
'buenos aires' => ['temp' => '24°C', 'condition' => 'Rainy', 'humidity' => '80%'],
54+
];
55+
56+
$key = strtolower($city);
57+
$data = $weather[$key] ?? ['temp' => '20°C', 'condition' => 'Clear', 'humidity' => '60%'];
58+
59+
return \sprintf(
60+
'Weather in %s: %s, %s, Humidity: %s',
61+
$city,
62+
$data['temp'],
63+
$data['condition'],
64+
$data['humidity'],
65+
);
66+
}
67+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#!/usr/bin/env php
2+
<?php
3+
4+
/*
5+
* This file is part of the official PHP MCP SDK.
6+
*
7+
* A collaboration between Symfony and the PHP Foundation.
8+
*
9+
* For the full copyright and license information, please view the LICENSE
10+
* file that was distributed with this source code.
11+
*/
12+
13+
require_once dirname(__DIR__).'/bootstrap.php';
14+
chdir(__DIR__);
15+
16+
use Mcp\Example\Server\McpApps\WeatherApp;
17+
use Mcp\Schema\Extension\Apps\McpApps;
18+
use Mcp\Schema\Extension\Apps\ToolVisibility;
19+
use Mcp\Schema\Extension\Apps\UiToolMeta;
20+
use Mcp\Server;
21+
22+
logger()->info('Starting MCP Apps Example Server...');
23+
24+
$server = Server::builder()
25+
->setServerInfo('MCP Apps Weather Example', '1.0.0')
26+
->setLogger(logger())
27+
->enableExtension(McpApps::class)
28+
->addResource(
29+
[WeatherApp::class, 'getWeatherApp'],
30+
'ui://weather-app',
31+
'weather-app',
32+
description: 'Interactive weather dashboard',
33+
mimeType: McpApps::MIME_TYPE,
34+
meta: ['ui' => new stdClass()],
35+
)
36+
->addTool(
37+
[WeatherApp::class, 'getWeather'],
38+
'get_weather',
39+
description: 'Get current weather for a city',
40+
meta: ['ui' => new UiToolMeta(
41+
resourceUri: 'ui://weather-app',
42+
visibility: [ToolVisibility::Model->value, ToolVisibility::App->value],
43+
)],
44+
)
45+
->build();
46+
47+
$result = $server->run(transport());
48+
49+
logger()->info('Server stopped gracefully.', ['result' => $result]);
50+
51+
shutdown($result);

0 commit comments

Comments
 (0)