Skip to content
Draft
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
194 changes: 194 additions & 0 deletions examples/server/mcp-apps/WeatherApp.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?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;

/**
* Example MCP Apps server exposing an interactive weather dashboard.
*
* The server provides:
* - A UI resource at ui://weather-app that returns an HTML weather dashboard
* - A tool "get_weather" linked to the UI resource, callable by both the model and the app
*/
final class WeatherApp
{
/**
* Returns the HTML content for the weather dashboard UI resource.
*
* This is registered as a resource with the ui:// URI scheme and the
* MCP App MIME type. The host application will render it in a sandboxed iframe.
*/
public function getWeatherApp(): TextResourceContents
{
$html = <<<'HTML'
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-sans, system-ui, sans-serif);
background: var(--color-background-primary, #ffffff);
color: var(--color-text-primary, #1a1a1a);
padding: 1rem;
}
.card {
border: 1px solid var(--color-border-primary, #e0e0e0);
border-radius: var(--border-radius-md, 8px);
padding: 1rem;
margin-bottom: 1rem;
}
h1 { font-size: var(--font-heading-md-size, 1.25rem); margin-bottom: 0.5rem; }
.weather-data { font-size: var(--font-text-lg-size, 1.125rem); }
button {
background: var(--color-background-info, #0066cc);
color: var(--color-text-inverse, #ffffff);
border: none;
border-radius: var(--border-radius-sm, 4px);
padding: 0.5rem 1rem;
cursor: pointer;
font-size: var(--font-text-md-size, 1rem);
}
input {
border: 1px solid var(--color-border-primary, #e0e0e0);
border-radius: var(--border-radius-sm, 4px);
padding: 0.5rem;
font-size: var(--font-text-md-size, 1rem);
margin-right: 0.5rem;
}
</style>
</head>
<body>
<div class="card">
<h1>Weather Dashboard</h1>
<div>
<input type="text" id="city" placeholder="Enter city name" value="London">
<button onclick="fetchWeather()">Get Weather</button>
</div>
</div>
<div class="card" id="result" style="display:none">
<div class="weather-data" id="weather-data"></div>
</div>
<script>
// MCP Apps communicate with the host via window.parent.postMessage
// using JSON-RPC 2.0 messages.
let requestId = 0;
const pending = new Map();

window.addEventListener('message', (event) => {
try {
const msg = JSON.parse(event.data);
if (msg.id && pending.has(msg.id)) {
pending.get(msg.id)(msg);
pending.delete(msg.id);
}
// Handle tool input notification
if (msg.method === 'ui/notifications/tool-input') {
document.getElementById('city').value = msg.params.arguments.city || '';
}
// Handle tool result notification
if (msg.method === 'ui/notifications/tool-result') {
displayResult(msg.params);
}
} catch (e) { /* ignore non-JSON messages */ }
});

function sendRpc(method, params) {
return new Promise((resolve) => {
const id = ++requestId;
pending.set(id, resolve);
window.parent.postMessage(JSON.stringify({
jsonrpc: '2.0', id, method, params
}), '*');
});
}

async function fetchWeather() {
const city = document.getElementById('city').value;
const response = await sendRpc('tools/call', {
name: 'get_weather',
arguments: { city }
});
if (response.result) {
displayResult(response.result);
}
}

function displayResult(result) {
const el = document.getElementById('result');
const data = document.getElementById('weather-data');
if (result.content && result.content[0]) {
data.textContent = result.content[0].text;
}
el.style.display = 'block';
}
</script>
</body>
</html>
HTML;

$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: $html,
meta: $contentMeta->toMetaArray(),
);
}

/**
* Returns weather data for a given city.
*
* This tool is linked to the ui://weather-app UI resource via _meta.ui.resourceUri,
* making it callable by both the LLM agent and the rendered HTML app.
*
* @return string simulated weather data
*/
public function getWeather(string $city): string
{
// In a real application, this would call an external weather API.
$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%'],
];

$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'],
);
}
}
92 changes: 92 additions & 0 deletions examples/server/mcp-apps/server.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/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\Enum\ToolVisibility;
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Schema\Extension\Apps\UiToolMeta;
use Mcp\Schema\ServerCapabilities;
use Mcp\Server;

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

// Build the tool UI metadata using the typed helper class.
// This links the "get_weather" tool to the "ui://weather-app" UI resource.
$toolUiMeta = new UiToolMeta(
resourceUri: 'ui://weather-app',
visibility: [ToolVisibility::Model->value, ToolVisibility::App->value],
);

$server = Server::builder()
->setServerInfo('MCP Apps Weather Example', '1.0.0')
->setLogger(logger())
->setContainer(container())

// Register the UI resource with ui:// scheme and MCP App MIME type.
// The _meta marks this as a UI resource in the resources/list response.
->addResource(
[WeatherApp::class, 'getWeatherApp'],
'ui://weather-app',
'weather-app',
description: 'Interactive weather dashboard',
mimeType: McpApps::MIME_TYPE,
meta: ['ui' => []],
)

// Register the tool linked to the UI resource via _meta.ui.
->addTool(
[WeatherApp::class, 'getWeather'],
'get_weather',
description: 'Get current weather for a city',
meta: $toolUiMeta->toMetaArray(),
)

// Advertise MCP Apps support in server capabilities.
->setCapabilities(new ServerCapabilities(
tools: true,
resources: true,
prompts: false,
extensions: [
McpApps::EXTENSION_ID => McpApps::extensionCapability(),
],
))
->build();

/*
* Equivalent attribute-based registration (PHP attributes require constant expressions,
* so _meta must be specified as a raw array literal):
*
* #[McpResource(
* uri: 'ui://weather-app',
* name: 'weather-app',
* description: 'Interactive weather dashboard',
* mimeType: 'text/html;profile=mcp-app',
* meta: ['ui' => []],
* )]
* public function getWeatherApp(): TextResourceContents { ... }
*
* #[McpTool(
* name: 'get_weather',
* description: 'Get current weather for a city',
* meta: ['ui' => ['resourceUri' => 'ui://weather-app', 'visibility' => ['model', 'app']]],
* )]
* public function getWeather(string $city): string { ... }
*/

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

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

shutdown($result);
13 changes: 11 additions & 2 deletions src/Schema/ClientCapabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@
class ClientCapabilities implements \JsonSerializable
{
/**
* @param array<string, mixed> $experimental
* @param array<string, mixed> $experimental
* @param ?array<string, mixed> $extensions protocol extensions the client supports (e.g. io.modelcontextprotocol/ui)
*/
public function __construct(
public readonly ?bool $roots = false,
public readonly ?bool $rootsListChanged = null,
public readonly ?bool $sampling = null,
public readonly ?bool $elicitation = null,
public readonly ?array $experimental = null,
public readonly ?array $extensions = null,
) {
}

Expand All @@ -39,6 +41,7 @@ public function __construct(
* sampling?: bool,
* elicitation?: bool,
* experimental?: array<string, mixed>,
* extensions?: array<string, mixed>,
* } $data
*/
public static function fromArray(array $data): self
Expand Down Expand Up @@ -68,7 +71,8 @@ public static function fromArray(array $data): self
$rootsListChanged,
$sampling,
$elicitation,
$data['experimental'] ?? null
$data['experimental'] ?? null,
$data['extensions'] ?? null,
);
}

Expand All @@ -78,6 +82,7 @@ public static function fromArray(array $data): self
* sampling?: object,
* elicitation?: object,
* experimental?: object,
* extensions?: object,
* }
*/
public function jsonSerialize(): array
Expand All @@ -102,6 +107,10 @@ public function jsonSerialize(): array
$data['experimental'] = (object) $this->experimental;
}

if ($this->extensions) {
$data['extensions'] = (object) $this->extensions;
}

return $data;
}
}
26 changes: 26 additions & 0 deletions src/Schema/Enum/ToolVisibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?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\Schema\Enum;

/**
* Tool visibility values for MCP Apps.
*
* Controls who can see and invoke a tool linked to a UI resource.
*/
enum ToolVisibility: string
{
/** Visible to and callable by the LLM agent. */
case Model = 'model';

/** Callable by the MCP App (HTML view) only, hidden from the model's tools/list. */
case App = 'app';
}
Loading
Loading