Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"Mcp\\Example\\Server\\DiscoveryUserProfile\\": "examples/server/discovery-userprofile/",
"Mcp\\Example\\Server\\EnvVariables\\": "examples/server/env-variables/",
"Mcp\\Example\\Server\\ExplicitRegistration\\": "examples/server/explicit-registration/",
"Mcp\\Example\\Server\\McpApps\\": "examples/server/mcp-apps/",
"Mcp\\Example\\Server\\OAuthKeycloak\\": "examples/server/oauth-keycloak/",
"Mcp\\Example\\Server\\OAuthMicrosoft\\": "examples/server/oauth-microsoft/",
"Mcp\\Example\\Server\\SchemaShowcase\\": "examples/server/schema-showcase/",
Expand Down
12 changes: 12 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,18 @@ npx @modelcontextprotocol/inspector php examples/server/elicitation/server.php
2. **confirm_action** - Simple boolean confirmation dialog
3. **collect_feedback** - Rating and comments form with optional fields

### MCP Apps

**File**: `examples/server/mcp-apps/`

A weather app demonstrating the [MCP Apps extension](extensions.md): a `ui://`
HTML resource is opened by an MCP App-aware client (e.g. Goose) and bridged to
the `get_weather` tool. The bundled `weather-app.html` performs the
`ui/initialize` handshake, reports its size via `ui/notifications/size-changed`,
and calls back into the server. See the
[ext-apps repo](https://github.com/modelcontextprotocol/ext-apps) for the
TypeScript SDK and richer view-side patterns.

## Client Examples

### STDIO Discovery Calculator (Client)
Expand Down
103 changes: 103 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Protocol Extensions

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

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

$server = Server::builder()
->setServerInfo('My Server', '1.0.0')
->enableExtension(McpApps::class) // or pre-built instances
->build();
```

Pass either a class string (the extension is instantiated with no arguments) or
a pre-built `ServerExtensionInterface` instance. Multiple extensions can be
enabled in a single call.

> Note: calling `setCapabilities()` overrides automatic capability detection,
> so it also overrides the `extensions` field. If you set your own
> `ServerCapabilities`, include the extensions you want yourself.

## MCP Apps (`io.modelcontextprotocol/ui`)

The [MCP Apps extension][ext-apps] 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.

```php
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(McpApps::class)
->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' => new \stdClass()],
)
->addTool(
$myToolHandler,
'my_tool',
meta: ['ui' => new UiToolMeta(
resourceUri: 'ui://my-app',
visibility: [ToolVisibility::Model->value, ToolVisibility::App->value],
)],
)
->build();
```

### 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][ext-apps] 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`](../examples/server/mcp-apps/weather-app.html).

[ext-apps]: https://github.com/modelcontextprotocol/ext-apps
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
- [Client](client.md) — Client SDK for connecting to and communicating with MCP servers.
- [Transports](transports.md) — STDIO and HTTP transport implementations with guidance on choosing between them.
- [Server-Client Communication](server-client-communication.md) — Methods for servers to communicate back to clients: sampling, logging, progress, and notifications.
- [Protocol Extensions](extensions.md) — Opt-in protocol extensions announced during capability negotiation, including MCP Apps (HTML UI resources).
- [Examples](examples.md) — Example projects demonstrating attribute-based discovery, dependency injection, HTTP transport, and more.
67 changes: 67 additions & 0 deletions examples/server/mcp-apps/WeatherApp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Example\Server\McpApps;

use Mcp\Schema\Content\TextResourceContents;
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Schema\Extension\Apps\UiResourceContentMeta;
use Mcp\Schema\Extension\Apps\UiResourceCsp;
use Mcp\Schema\Extension\Apps\UiResourcePermissions;

final class WeatherApp
{
public function getWeatherApp(): TextResourceContents
{
$contentMeta = new UiResourceContentMeta(
csp: new UiResourceCsp(
connectDomains: ['https://api.weather.example.com'],
),
permissions: new UiResourcePermissions(
geolocation: true,
),
prefersBorder: true,
);

return new TextResourceContents(
uri: 'ui://weather-app',
mimeType: McpApps::MIME_TYPE,
text: file_get_contents(__DIR__.'/weather-app.html'),
meta: ['ui' => $contentMeta],
);
}

public function getWeather(string $city): string
{
$weather = [
'london' => ['temp' => '15°C', 'condition' => 'Cloudy', 'humidity' => '78%'],
'paris' => ['temp' => '18°C', 'condition' => 'Sunny', 'humidity' => '55%'],
'tokyo' => ['temp' => '22°C', 'condition' => 'Partly Cloudy', 'humidity' => '65%'],
'new york' => ['temp' => '12°C', 'condition' => 'Rainy', 'humidity' => '85%'],
'lagos' => ['temp' => '30°C', 'condition' => 'Sunny', 'humidity' => '82%'],
'stockholm' => ['temp' => '4°C', 'condition' => 'Cloudy', 'humidity' => '70%'],
'berlin' => ['temp' => '9°C', 'condition' => 'Partly Cloudy', 'humidity' => '68%'],
'sydney' => ['temp' => '26°C', 'condition' => 'Sunny', 'humidity' => '60%'],
'buenos aires' => ['temp' => '24°C', 'condition' => 'Rainy', 'humidity' => '80%'],
];

$key = strtolower($city);
$data = $weather[$key] ?? ['temp' => '20°C', 'condition' => 'Clear', 'humidity' => '60%'];

return \sprintf(
'Weather in %s: %s, %s, Humidity: %s',
$city,
$data['temp'],
$data['condition'],
$data['humidity'],
);
}
}
51 changes: 51 additions & 0 deletions examples/server/mcp-apps/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env php
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

require_once dirname(__DIR__).'/bootstrap.php';
chdir(__DIR__);

use Mcp\Example\Server\McpApps\WeatherApp;
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Schema\Extension\Apps\ToolVisibility;
use Mcp\Schema\Extension\Apps\UiToolMeta;
use Mcp\Server;

logger()->info('Starting MCP Apps Example Server...');

$server = Server::builder()
->setServerInfo('MCP Apps Weather Example', '1.0.0')
->setLogger(logger())
->enableExtension(McpApps::class)
->addResource(
[WeatherApp::class, 'getWeatherApp'],
'ui://weather-app',
'weather-app',
description: 'Interactive weather dashboard',
mimeType: McpApps::MIME_TYPE,
meta: ['ui' => new stdClass()],
)
->addTool(
[WeatherApp::class, 'getWeather'],
'get_weather',
description: 'Get current weather for a city',
meta: ['ui' => new UiToolMeta(
resourceUri: 'ui://weather-app',
visibility: [ToolVisibility::Model->value, ToolVisibility::App->value],
)],
)
->build();

$result = $server->run(transport());

logger()->info('Server stopped gracefully.', ['result' => $result]);

shutdown($result);
Loading