Add title field to Resource and ResourceTemplate (#301)

The MCP specification (2025-11-25) defines an optional title field for
Resource and ResourceTemplate as a human-readable display label, distinct
from name (a short identifier). Tool and Prompt already implement this
field; Resource and ResourceTemplate were missing it.

Changes:
- Add title parameter to Resource and ResourceTemplate constructors
- Add title to fromArray(), jsonSerialize(), and PHPStan type arrays
- Add title to McpResource and McpResourceTemplate attributes
- Pass title through Discoverer when constructing schema objects
- Add tests for deserialization, serialization, and null omission

Fixes #296
This commit is contained in:
Dayna Blackwell 2026-05-17 05:10:31 -07:00 • committed by GitHub
commit a820497b62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 110 additions and 21 deletions

View file

@ -24,18 +24,20 @@ use Mcp\Schema\Icon;
class McpResource
{
/**
* @param string $uri The specific URI identifying this resource instance. Must be unique within the server.
* @param ?string $name A human-readable name for this resource. If null, a default might be generated from the method name.
* @param ?string $description An optional description of the resource. Defaults to class DocBlock summary.
* @param string $uri the specific URI identifying this resource instance
* @param ?string $name a short identifier for this resource; defaults to the method name
* @param ?string $title optional human-readable title for display in UI
* @param ?string $description optional description; defaults to class DocBlock summary
* @param ?string $mimeType the MIME type, if known and constant for this resource
* @param ?int $size the size in bytes, if known and constant
* @param Annotations|null $annotations optional annotations describing the resource
* @param ?Icon[] $icons Optional list of icon URLs representing the resource
* @param ?array<string, mixed> $meta Optional metadata
* @param ?Annotations $annotations optional annotations describing the resource
* @param ?Icon[] $icons optional icons representing the resource
* @param ?array<string, mixed> $meta optional metadata
*/
public function __construct(
public string $uri,
public ?string $name = null,
public ?string $title = null,
public ?string $description = null,
public ?string $mimeType = null,
public ?int $size = null,

View file

@ -24,15 +24,17 @@ class McpResourceTemplate
{
/**
* @param string $uriTemplate the URI template string (RFC 6570)
* @param ?string $name A human-readable name for the template type. If null, a default might be generated from the method name.
* @param ?string $description Optional description. Defaults to class DocBlock summary.
* @param ?string $name a short identifier for the template type; defaults to the method name
* @param ?string $title optional human-readable title for display in UI
* @param ?string $description optional description; defaults to class DocBlock summary
* @param ?string $mimeType optional default MIME type for matching resources
* @param ?Annotations $annotations optional annotations describing the resource template
* @param ?array<string, mixed> $meta Optional metadata
* @param ?array<string, mixed> $meta optional metadata
*/
public function __construct(
public string $uriTemplate,
public ?string $name = null,
public ?string $title = null,
public ?string $description = null,
public ?string $mimeType = null,
public ?Annotations $annotations = null,

View file

@ -252,6 +252,7 @@ final class Discoverer implements DiscovererInterface
$resource = new Resource(
$instance->uri,
$name,
$instance->title,
$description,
$instance->mimeType,
$instance->annotations,
@ -291,7 +292,7 @@ final class Discoverer implements DiscovererInterface
$mimeType = $instance->mimeType;
$annotations = $instance->annotations;
$meta = $instance->meta ?? null;
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $description, $mimeType, $annotations, $meta);
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $instance->title, $description, $mimeType, $annotations, $meta);
$completionProviders = $this->getCompletionProviders($method);
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], $completionProviders);
++$discoveredCount['resourceTemplates'];

View file

@ -22,6 +22,7 @@ use Mcp\Exception\InvalidArgumentException;
* @phpstan-type ResourceData array{
* uri: string,
* name: string,
* title?: string,
* description?: string,
* mimeType?: string,
* annotations?: AnnotationsData,
@ -47,19 +48,19 @@ class Resource implements \JsonSerializable
/**
* @param string $uri the URI of this resource
* @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements.
* @param ?string $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
* @param string $name a short identifier for this resource
* @param ?string $title optional human-readable title for display in UI
* @param ?string $description A description of what this resource represents. This can be used by clients to improve the LLM's understanding of available resources.
* @param ?string $mimeType the MIME type of this resource, if known
* @param ?Annotations $annotations optional annotations for the client
* @param ?int $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
* @param ?int $size the size of the raw resource content, in bytes (before base64 encoding or any tokenization), if known
* @param ?Icon[] $icons optional icons representing the resource
* @param ?array<string, mixed> $meta Optional metadata
*
* This can be used by Hosts to display file sizes and estimate context window usage
* @param ?array<string, mixed> $meta optional metadata
*/
public function __construct(
public readonly string $uri,
public readonly string $name,
public readonly ?string $title = null,
public readonly ?string $description = null,
public readonly ?string $mimeType = null,
public readonly ?Annotations $annotations = null,
@ -94,6 +95,7 @@ class Resource implements \JsonSerializable
return new self(
uri: $data['uri'],
name: $data['name'],
title: isset($data['title']) && \is_string($data['title']) ? $data['title'] : null,
description: $data['description'] ?? null,
mimeType: $data['mimeType'] ?? null,
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
@ -107,6 +109,7 @@ class Resource implements \JsonSerializable
* @return array{
* uri: string,
* name: string,
* title?: string,
* description?: string,
* mimeType?: string,
* annotations?: Annotations,
@ -121,6 +124,9 @@ class Resource implements \JsonSerializable
'uri' => $this->uri,
'name' => $this->name,
];
if (null !== $this->title) {
$data['title'] = $this->title;
}
if (null !== $this->description) {
$data['description'] = $this->description;
}

View file

@ -21,6 +21,7 @@ use Mcp\Exception\InvalidArgumentException;
* @phpstan-type ResourceTemplateData array{
* uriTemplate: string,
* name: string,
* title?: string,
* description?: string|null,
* mimeType?: string|null,
* annotations?: AnnotationsData|null,
@ -44,15 +45,17 @@ class ResourceTemplate implements \JsonSerializable
/**
* @param string $uriTemplate a URI template (according to RFC 6570) that can be used to construct resource URIs
* @param string $name A human-readable name for the type of resource this template refers to. This can be used by clients to populate UI elements.
* @param string|null $description This can be used by clients to improve the LLM's understanding of available resources. It can be thought of like a "hint" to the model.
* @param string|null $mimeType The MIME type for all resources that match this template. This should only be included if all resources matching this template have the same type.
* @param Annotations|null $annotations optional annotations for the client
* @param ?array<string, mixed> $meta Optional metadata
* @param string $name a short identifier for this resource template type
* @param ?string $title optional human-readable title for display in UI
* @param ?string $description a description to help the LLM understand available resources
* @param ?string $mimeType the MIME type for all resources that match this template, if uniform
* @param ?Annotations $annotations optional annotations for the client
* @param ?array<string, mixed> $meta optional metadata
*/
public function __construct(
public readonly string $uriTemplate,
public readonly string $name,
public readonly ?string $title = null,
public readonly ?string $description = null,
public readonly ?string $mimeType = null,
public readonly ?Annotations $annotations = null,
@ -85,6 +88,7 @@ class ResourceTemplate implements \JsonSerializable
return new self(
uriTemplate: $data['uriTemplate'],
name: $data['name'],
title: isset($data['title']) && \is_string($data['title']) ? $data['title'] : null,
description: $data['description'] ?? null,
mimeType: $data['mimeType'] ?? null,
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
@ -96,6 +100,7 @@ class ResourceTemplate implements \JsonSerializable
* @return array{
* uriTemplate: string,
* name: string,
* title?: string,
* description?: string,
* mimeType?: string,
* annotations?: Annotations,
@ -108,6 +113,9 @@ class ResourceTemplate implements \JsonSerializable
'uriTemplate' => $this->uriTemplate,
'name' => $this->name,
];
if (null !== $this->title) {
$data['title'] = $this->title;
}
if (null !== $this->description) {
$data['description'] = $this->description;
}

View file

@ -75,10 +75,45 @@ class ResourceTemplateTest extends TestCase
$this->assertInstanceOf(ResourceTemplate::class, $resource);
$this->assertSame(self::VALID_URI, $resource->uriTemplate);
$this->assertSame('list-books', $resource->name);
$this->assertNull($resource->title);
$this->assertNull($resource->description);
$this->assertNull($resource->meta);
}
public function testTitleFromArray(): void
{
$resource = ResourceTemplate::fromArray([
'uriTemplate' => self::VALID_URI,
'name' => 'list-books',
'title' => 'Book Listing',
]);
$this->assertSame('Book Listing', $resource->title);
}
public function testTitleSerialization(): void
{
$resource = new ResourceTemplate(
uriTemplate: self::VALID_URI,
name: 'list-books',
title: 'Book Listing',
);
$data = $resource->jsonSerialize();
$this->assertSame('Book Listing', $data['title']);
}
public function testTitleOmittedWhenNull(): void
{
$resource = new ResourceTemplate(
uriTemplate: self::VALID_URI,
name: 'list-books',
);
$data = $resource->jsonSerialize();
$this->assertArrayNotHasKey('title', $data);
}
#[DataProvider('provideInvalidResources')]
public function testFromArrayInvalid(array $input, string $expectedExceptionMessage): void
{

View file

@ -77,10 +77,45 @@ class ResourceTest extends TestCase
$this->assertInstanceOf(Resource::class, $resource);
$this->assertSame(self::VALID_URI, $resource->uri);
$this->assertSame('list-books', $resource->name);
$this->assertNull($resource->title);
$this->assertNull($resource->description);
$this->assertNull($resource->meta);
}
public function testTitleFromArray(): void
{
$resource = Resource::fromArray([
'uri' => self::VALID_URI,
'name' => 'list-books',
'title' => 'Book Listing',
]);
$this->assertSame('Book Listing', $resource->title);
}
public function testTitleSerialization(): void
{
$resource = new Resource(
uri: self::VALID_URI,
name: 'list-books',
title: 'Book Listing',
);
$data = $resource->jsonSerialize();
$this->assertSame('Book Listing', $data['title']);
}
public function testTitleOmittedWhenNull(): void
{
$resource = new Resource(
uri: self::VALID_URI,
name: 'list-books',
);
$data = $resource->jsonSerialize();
$this->assertArrayNotHasKey('title', $data);
}
#[DataProvider('provideInvalidResources')]
public function testFromArrayInvalid(array $input, string $expectedExceptionMessage): void
{