Skip to content

Commit d9ca7e6

Browse files
chr-hertelclaude
andcommitted
feat: add MCP Apps extension (io.modelcontextprotocol/ui) support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 863cfc1 commit d9ca7e6

File tree

13 files changed

+1185
-3
lines changed

13 files changed

+1185
-3
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
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/",
@@ -89,4 +90,4 @@
8990
},
9091
"sort-packages": true
9192
}
92-
}
93+
}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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+
/**
21+
* Example MCP Apps server exposing an interactive weather dashboard.
22+
*
23+
* The server provides:
24+
* - A UI resource at ui://weather-app that returns an HTML weather dashboard
25+
* - A tool "get_weather" linked to the UI resource, callable by both the model and the app
26+
*/
27+
final class WeatherApp
28+
{
29+
/**
30+
* Returns the HTML content for the weather dashboard UI resource.
31+
*
32+
* This is registered as a resource with the ui:// URI scheme and the
33+
* MCP App MIME type. The host application will render it in a sandboxed iframe.
34+
*/
35+
public function getWeatherApp(): TextResourceContents
36+
{
37+
$html = <<<'HTML'
38+
<!DOCTYPE html>
39+
<html lang="en">
40+
<head>
41+
<meta charset="UTF-8">
42+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
43+
<title>Weather Dashboard</title>
44+
<style>
45+
* { margin: 0; padding: 0; box-sizing: border-box; }
46+
body {
47+
font-family: var(--font-sans, system-ui, sans-serif);
48+
background: var(--color-background-primary, #ffffff);
49+
color: var(--color-text-primary, #1a1a1a);
50+
padding: 1rem;
51+
}
52+
.card {
53+
border: 1px solid var(--color-border-primary, #e0e0e0);
54+
border-radius: var(--border-radius-md, 8px);
55+
padding: 1rem;
56+
margin-bottom: 1rem;
57+
}
58+
h1 { font-size: var(--font-heading-md-size, 1.25rem); margin-bottom: 0.5rem; }
59+
.weather-data { font-size: var(--font-text-lg-size, 1.125rem); }
60+
button {
61+
background: var(--color-background-info, #0066cc);
62+
color: var(--color-text-inverse, #ffffff);
63+
border: none;
64+
border-radius: var(--border-radius-sm, 4px);
65+
padding: 0.5rem 1rem;
66+
cursor: pointer;
67+
font-size: var(--font-text-md-size, 1rem);
68+
}
69+
input {
70+
border: 1px solid var(--color-border-primary, #e0e0e0);
71+
border-radius: var(--border-radius-sm, 4px);
72+
padding: 0.5rem;
73+
font-size: var(--font-text-md-size, 1rem);
74+
margin-right: 0.5rem;
75+
}
76+
</style>
77+
</head>
78+
<body>
79+
<div class="card">
80+
<h1>Weather Dashboard</h1>
81+
<div>
82+
<input type="text" id="city" placeholder="Enter city name" value="London">
83+
<button onclick="fetchWeather()">Get Weather</button>
84+
</div>
85+
</div>
86+
<div class="card" id="result" style="display:none">
87+
<div class="weather-data" id="weather-data"></div>
88+
</div>
89+
<script>
90+
// MCP Apps communicate with the host via window.parent.postMessage
91+
// using JSON-RPC 2.0 messages.
92+
let requestId = 0;
93+
const pending = new Map();
94+
95+
window.addEventListener('message', (event) => {
96+
try {
97+
const msg = JSON.parse(event.data);
98+
if (msg.id && pending.has(msg.id)) {
99+
pending.get(msg.id)(msg);
100+
pending.delete(msg.id);
101+
}
102+
// Handle tool input notification
103+
if (msg.method === 'ui/notifications/tool-input') {
104+
document.getElementById('city').value = msg.params.arguments.city || '';
105+
}
106+
// Handle tool result notification
107+
if (msg.method === 'ui/notifications/tool-result') {
108+
displayResult(msg.params);
109+
}
110+
} catch (e) { /* ignore non-JSON messages */ }
111+
});
112+
113+
function sendRpc(method, params) {
114+
return new Promise((resolve) => {
115+
const id = ++requestId;
116+
pending.set(id, resolve);
117+
window.parent.postMessage(JSON.stringify({
118+
jsonrpc: '2.0', id, method, params
119+
}), '*');
120+
});
121+
}
122+
123+
async function fetchWeather() {
124+
const city = document.getElementById('city').value;
125+
const response = await sendRpc('tools/call', {
126+
name: 'get_weather',
127+
arguments: { city }
128+
});
129+
if (response.result) {
130+
displayResult(response.result);
131+
}
132+
}
133+
134+
function displayResult(result) {
135+
const el = document.getElementById('result');
136+
const data = document.getElementById('weather-data');
137+
if (result.content && result.content[0]) {
138+
data.textContent = result.content[0].text;
139+
}
140+
el.style.display = 'block';
141+
}
142+
</script>
143+
</body>
144+
</html>
145+
HTML;
146+
147+
$contentMeta = new UiResourceContentMeta(
148+
csp: new UiResourceCsp(
149+
connectDomains: ['https://api.weather.example.com'],
150+
),
151+
permissions: new UiResourcePermissions(
152+
geolocation: true,
153+
),
154+
prefersBorder: true,
155+
);
156+
157+
return new TextResourceContents(
158+
uri: 'ui://weather-app',
159+
mimeType: McpApps::MIME_TYPE,
160+
text: $html,
161+
meta: $contentMeta->toMetaArray(),
162+
);
163+
}
164+
165+
/**
166+
* Returns weather data for a given city.
167+
*
168+
* This tool is linked to the ui://weather-app UI resource via _meta.ui.resourceUri,
169+
* making it callable by both the LLM agent and the rendered HTML app.
170+
*
171+
* @return string simulated weather data
172+
*/
173+
public function getWeather(string $city): string
174+
{
175+
// In a real application, this would call an external weather API.
176+
$weather = [
177+
'london' => ['temp' => '15°C', 'condition' => 'Cloudy', 'humidity' => '78%'],
178+
'paris' => ['temp' => '18°C', 'condition' => 'Sunny', 'humidity' => '55%'],
179+
'tokyo' => ['temp' => '22°C', 'condition' => 'Partly Cloudy', 'humidity' => '65%'],
180+
'new york' => ['temp' => '12°C', 'condition' => 'Rainy', 'humidity' => '85%'],
181+
];
182+
183+
$key = strtolower($city);
184+
$data = $weather[$key] ?? ['temp' => '20°C', 'condition' => 'Clear', 'humidity' => '60%'];
185+
186+
return \sprintf(
187+
'Weather in %s: %s, %s, Humidity: %s',
188+
$city,
189+
$data['temp'],
190+
$data['condition'],
191+
$data['humidity'],
192+
);
193+
}
194+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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\Enum\ToolVisibility;
18+
use Mcp\Schema\Extension\Apps\McpApps;
19+
use Mcp\Schema\Extension\Apps\UiToolMeta;
20+
use Mcp\Schema\ServerCapabilities;
21+
use Mcp\Server;
22+
23+
logger()->info('Starting MCP Apps Example Server...');
24+
25+
// Build the tool UI metadata using the typed helper class.
26+
// This links the "get_weather" tool to the "ui://weather-app" UI resource.
27+
$toolUiMeta = new UiToolMeta(
28+
resourceUri: 'ui://weather-app',
29+
visibility: [ToolVisibility::Model->value, ToolVisibility::App->value],
30+
);
31+
32+
$server = Server::builder()
33+
->setServerInfo('MCP Apps Weather Example', '1.0.0')
34+
->setLogger(logger())
35+
->setContainer(container())
36+
37+
// Register the UI resource with ui:// scheme and MCP App MIME type.
38+
// The _meta marks this as a UI resource in the resources/list response.
39+
->addResource(
40+
[WeatherApp::class, 'getWeatherApp'],
41+
'ui://weather-app',
42+
'weather-app',
43+
description: 'Interactive weather dashboard',
44+
mimeType: McpApps::MIME_TYPE,
45+
meta: ['ui' => []],
46+
)
47+
48+
// Register the tool linked to the UI resource via _meta.ui.
49+
->addTool(
50+
[WeatherApp::class, 'getWeather'],
51+
'get_weather',
52+
description: 'Get current weather for a city',
53+
meta: $toolUiMeta->toMetaArray(),
54+
)
55+
56+
// Advertise MCP Apps support in server capabilities.
57+
->setCapabilities(new ServerCapabilities(
58+
tools: true,
59+
resources: true,
60+
prompts: false,
61+
extensions: [
62+
McpApps::EXTENSION_ID => McpApps::extensionCapability(),
63+
],
64+
))
65+
->build();
66+
67+
/*
68+
* Equivalent attribute-based registration (PHP attributes require constant expressions,
69+
* so _meta must be specified as a raw array literal):
70+
*
71+
* #[McpResource(
72+
* uri: 'ui://weather-app',
73+
* name: 'weather-app',
74+
* description: 'Interactive weather dashboard',
75+
* mimeType: 'text/html;profile=mcp-app',
76+
* meta: ['ui' => []],
77+
* )]
78+
* public function getWeatherApp(): TextResourceContents { ... }
79+
*
80+
* #[McpTool(
81+
* name: 'get_weather',
82+
* description: 'Get current weather for a city',
83+
* meta: ['ui' => ['resourceUri' => 'ui://weather-app', 'visibility' => ['model', 'app']]],
84+
* )]
85+
* public function getWeather(string $city): string { ... }
86+
*/
87+
88+
$result = $server->run(transport());
89+
90+
logger()->info('Server stopped gracefully.', ['result' => $result]);
91+
92+
shutdown($result);

src/Schema/ClientCapabilities.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
class ClientCapabilities implements \JsonSerializable
2121
{
2222
/**
23-
* @param array<string, mixed> $experimental
23+
* @param array<string, mixed> $experimental
24+
* @param ?array<string, mixed> $extensions protocol extensions the client supports (e.g. io.modelcontextprotocol/ui)
2425
*/
2526
public function __construct(
2627
public readonly ?bool $roots = false,
2728
public readonly ?bool $rootsListChanged = null,
2829
public readonly ?bool $sampling = null,
2930
public readonly ?bool $elicitation = null,
3031
public readonly ?array $experimental = null,
32+
public readonly ?array $extensions = null,
3133
) {
3234
}
3335

@@ -39,6 +41,7 @@ public function __construct(
3941
* sampling?: bool,
4042
* elicitation?: bool,
4143
* experimental?: array<string, mixed>,
44+
* extensions?: array<string, mixed>,
4245
* } $data
4346
*/
4447
public static function fromArray(array $data): self
@@ -68,7 +71,8 @@ public static function fromArray(array $data): self
6871
$rootsListChanged,
6972
$sampling,
7073
$elicitation,
71-
$data['experimental'] ?? null
74+
$data['experimental'] ?? null,
75+
$data['extensions'] ?? null,
7276
);
7377
}
7478

@@ -78,6 +82,7 @@ public static function fromArray(array $data): self
7882
* sampling?: object,
7983
* elicitation?: object,
8084
* experimental?: object,
85+
* extensions?: object,
8186
* }
8287
*/
8388
public function jsonSerialize(): array
@@ -102,6 +107,10 @@ public function jsonSerialize(): array
102107
$data['experimental'] = (object) $this->experimental;
103108
}
104109

110+
if ($this->extensions) {
111+
$data['extensions'] = (object) $this->extensions;
112+
}
113+
105114
return $data;
106115
}
107116
}

src/Schema/Enum/ToolVisibility.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Schema\Enum;
13+
14+
/**
15+
* Tool visibility values for MCP Apps.
16+
*
17+
* Controls who can see and invoke a tool linked to a UI resource.
18+
*/
19+
enum ToolVisibility: string
20+
{
21+
/** Visible to and callable by the LLM agent. */
22+
case Model = 'model';
23+
24+
/** Callable by the MCP App (HTML view) only, hidden from the model's tools/list. */
25+
case App = 'app';
26+
}

0 commit comments

Comments
 (0)