[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:
parent
590afb575b
commit
6f58fe596a
1 changed files with 273 additions and 21 deletions
|
|
@ -13,14 +13,37 @@ $server = Server::builder()
|
|||
->build();
|
||||
```
|
||||
|
||||
Pass one or more `ServerExtensionInterface` instances; multiple extensions can
|
||||
Pass one or more `ExtensionInterface` instances; multiple extensions can
|
||||
be enabled in a single call. Enabling the same extension twice throws a
|
||||
`LogicException`.
|
||||
|
||||
Clients (hosts) advertise the extensions they support the same way, via
|
||||
`Client\Builder::enableExtension()`; the payload lands under
|
||||
`capabilities.extensions` in the initialize request.
|
||||
|
||||
> Note: extensions enabled via `enableExtension()` are merged into the
|
||||
> `extensions` capability even when you supply your own `ServerCapabilities` via
|
||||
> `setCapabilities()`. An enabled extension overrides any entry under the same
|
||||
> id already present in those capabilities.
|
||||
> `extensions` capability even when you supply your own `ServerCapabilities` /
|
||||
> `ClientCapabilities` via `setCapabilities()`. An enabled extension overrides
|
||||
> any entry under the same id already present in those capabilities.
|
||||
|
||||
## Checking what the client negotiated
|
||||
|
||||
An extension is only in effect when both sides advertise it. On the server, a
|
||||
handler can ask the `ClientGateway` from the injected `RequestContext` whether the
|
||||
connected client did:
|
||||
|
||||
```php
|
||||
use Mcp\Schema\Extension\Apps\McpApps;
|
||||
use Mcp\Server\RequestContext;
|
||||
|
||||
public function getWeather(string $city, RequestContext $context): string
|
||||
{
|
||||
if (!$context->getClientGateway()->supportsExtension(McpApps::EXTENSION_ID)) {
|
||||
// text-only fallback for hosts without MCP Apps support
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## MCP Apps (`io.modelcontextprotocol/ui`)
|
||||
|
||||
|
|
@ -78,6 +101,30 @@ flagging it as an MCP App, while the resource *content* returned by `resources/r
|
|||
carries the structured `UiResourceContentMeta` with the actual CSP and permission
|
||||
configuration.
|
||||
|
||||
### Attribute-based discovery
|
||||
|
||||
The same linkage works with `#[McpResource]` / `#[McpTool]`, since both accept a
|
||||
`meta` array and PHP allows `new` in attribute arguments. The one difference is the
|
||||
descriptor marker: `McpApps::resourceMarker()` is a method call and cannot appear
|
||||
in an attribute, so spell it as `new \stdClass()` there.
|
||||
|
||||
```php
|
||||
use Mcp\Capability\Attribute\McpResource;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Mcp\Schema\Extension\Apps\McpApps;
|
||||
use Mcp\Schema\Extension\Apps\ToolVisibility;
|
||||
use Mcp\Schema\Extension\Apps\UiToolMeta;
|
||||
|
||||
final class WeatherApp
|
||||
{
|
||||
#[McpResource(uri: 'ui://my-app', mimeType: McpApps::MIME_TYPE, meta: ['ui' => new \stdClass()])]
|
||||
public function view(): TextResourceContents { /* as above */ }
|
||||
|
||||
#[McpTool(name: 'my_tool', meta: ['ui' => new UiToolMeta(resourceUri: 'ui://my-app', visibility: [ToolVisibility::Model, ToolVisibility::App])])]
|
||||
public function myTool(string $city): string { /* ... */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Server-side DTOs
|
||||
|
||||
| Class | Purpose |
|
||||
|
|
|
|||
Loading…
Reference in a new issue