[Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks (#429)

* [Schema][Client][Server] Close MCP Apps gaps: extension negotiation checks

Add ClientGateway::supportsExtension(), Client\Builder::enableExtension() and
ClientCapabilities::withExtensions(); introduce side-agnostic ExtensionInterface
and deprecate ServerExtensionInterface; document attribute-based UI linkage.

* Drop ServerExtensionInterface in favour of ExtensionInterface
This commit is contained in:
Christopher Hertel 2026-08-18 01:46:53 +02:00 • committed by GitHub
commit 6f58fe596a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 273 additions and 21 deletions

View file

@ -15,6 +15,7 @@ use Mcp\Capability\Completion\EnumCompletionProvider;
use Mcp\Capability\Completion\ListCompletionProvider;
use Mcp\Capability\Discovery\Discoverer;
use Mcp\Capability\Registry\ToolReference;
use Mcp\Schema\Extension\Apps\McpApps;
use Mcp\Tests\Unit\Capability\Attribute\CompletionProviderFixture;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\AlternativeFileNameToolHandler;
use Mcp\Tests\Unit\Capability\Discovery\Fixtures\DiscoverableToolHandler;
@ -38,7 +39,7 @@ class DiscoveryTest extends TestCase
$discovery = $this->discoverer->discover(__DIR__, ['Fixtures'], [], ['*.php', '*.inc']);
$tools = $discovery->getTools();
$this->assertCount(5, $tools);
$this->assertCount(6, $tools);
$this->assertArrayHasKey('greet_user', $tools);
$this->assertEquals('greet_user', $tools['greet_user']->tool->name);
@ -63,7 +64,7 @@ class DiscoveryTest extends TestCase
$this->assertArrayNotHasKey('static_tool_should_be_ignored', $tools);
$resources = $discovery->getResources();
$this->assertCount(3, $resources);
$this->assertCount(4, $resources);
$this->assertArrayHasKey('app://info/version', $resources);
$this->assertEquals('app_version', $resources['app://info/version']->resource->name);
@ -72,6 +73,14 @@ class DiscoveryTest extends TestCase
$this->assertArrayHasKey('invokable://config/status', $resources);
$this->assertEquals([InvocableResourceFixture::class, '__invoke'], $resources['invokable://config/status']->handler);
$this->assertArrayHasKey('ui://widget/clock', $resources);
$this->assertEquals(McpApps::MIME_TYPE, $resources['ui://widget/clock']->resource->mimeType);
$this->assertJsonStringEqualsJsonString('{"ui":{}}', json_encode($resources['ui://widget/clock']->resource->meta));
$this->assertJsonStringEqualsJsonString(
'{"ui":{"resourceUri":"ui://widget/clock","visibility":["app"]}}',
json_encode($tools['show_clock']->tool->meta),
);
$prompts = $discovery->getPrompts();
$this->assertCount(4, $prompts);

View file

@ -0,0 +1,53 @@
<?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\Tests\Unit\Capability\Discovery\Fixtures;
use Mcp\Capability\Attribute\McpResource;
use Mcp\Capability\Attribute\McpTool;
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\UiToolMeta;
/**
* An MCP App declared purely through attributes: a `ui://` resource flagged with the
* `_meta.ui` marker and a tool linked to it via `UiToolMeta`.
*/
class DiscoverableUiHandler
{
#[McpResource(
uri: 'ui://widget/clock',
name: 'clock_widget',
mimeType: McpApps::MIME_TYPE,
meta: ['ui' => new \stdClass()],
)]
public function widget(): TextResourceContents
{
return new TextResourceContents(
uri: 'ui://widget/clock',
mimeType: McpApps::MIME_TYPE,
text: '<html></html>',
meta: ['ui' => new UiResourceContentMeta(csp: new UiResourceCsp(connectDomains: ['https://time.example.com']))],
);
}
#[McpTool(
name: 'show_clock',
meta: ['ui' => new UiToolMeta(resourceUri: 'ui://widget/clock', visibility: [ToolVisibility::App])],
)]
public function showClock(string $timezone): string
{
return $timezone;
}
}