Skip to content

Commit a820497

Browse files
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
1 parent 967c5cd commit a820497

7 files changed

Lines changed: 110 additions & 21 deletions

File tree

src/Capability/Attribute/McpResource.php

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@
2424
class McpResource
2525
{
2626
/**
27-
* @param string $uri The specific URI identifying this resource instance. Must be unique within the server.
28-
* @param ?string $name A human-readable name for this resource. If null, a default might be generated from the method name.
29-
* @param ?string $description An optional description of the resource. Defaults to class DocBlock summary.
27+
* @param string $uri the specific URI identifying this resource instance
28+
* @param ?string $name a short identifier for this resource; defaults to the method name
29+
* @param ?string $title optional human-readable title for display in UI
30+
* @param ?string $description optional description; defaults to class DocBlock summary
3031
* @param ?string $mimeType the MIME type, if known and constant for this resource
3132
* @param ?int $size the size in bytes, if known and constant
32-
* @param Annotations|null $annotations optional annotations describing the resource
33-
* @param ?Icon[] $icons Optional list of icon URLs representing the resource
34-
* @param ?array<string, mixed> $meta Optional metadata
33+
* @param ?Annotations $annotations optional annotations describing the resource
34+
* @param ?Icon[] $icons optional icons representing the resource
35+
* @param ?array<string, mixed> $meta optional metadata
3536
*/
3637
public function __construct(
3738
public string $uri,
3839
public ?string $name = null,
40+
public ?string $title = null,
3941
public ?string $description = null,
4042
public ?string $mimeType = null,
4143
public ?int $size = null,

src/Capability/Attribute/McpResourceTemplate.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,17 @@ class McpResourceTemplate
2424
{
2525
/**
2626
* @param string $uriTemplate the URI template string (RFC 6570)
27-
* @param ?string $name A human-readable name for the template type. If null, a default might be generated from the method name.
28-
* @param ?string $description Optional description. Defaults to class DocBlock summary.
27+
* @param ?string $name a short identifier for the template type; defaults to the method name
28+
* @param ?string $title optional human-readable title for display in UI
29+
* @param ?string $description optional description; defaults to class DocBlock summary
2930
* @param ?string $mimeType optional default MIME type for matching resources
3031
* @param ?Annotations $annotations optional annotations describing the resource template
31-
* @param ?array<string, mixed> $meta Optional metadata
32+
* @param ?array<string, mixed> $meta optional metadata
3233
*/
3334
public function __construct(
3435
public string $uriTemplate,
3536
public ?string $name = null,
37+
public ?string $title = null,
3638
public ?string $description = null,
3739
public ?string $mimeType = null,
3840
public ?Annotations $annotations = null,

src/Capability/Discovery/Discoverer.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
252252
$resource = new Resource(
253253
$instance->uri,
254254
$name,
255+
$instance->title,
255256
$description,
256257
$instance->mimeType,
257258
$instance->annotations,
@@ -291,7 +292,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
291292
$mimeType = $instance->mimeType;
292293
$annotations = $instance->annotations;
293294
$meta = $instance->meta ?? null;
294-
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $description, $mimeType, $annotations, $meta);
295+
$resourceTemplate = new ResourceTemplate($instance->uriTemplate, $name, $instance->title, $description, $mimeType, $annotations, $meta);
295296
$completionProviders = $this->getCompletionProviders($method);
296297
$resourceTemplates[$instance->uriTemplate] = new ResourceTemplateReference($resourceTemplate, [$className, $methodName], $completionProviders);
297298
++$discoveredCount['resourceTemplates'];

src/Schema/Resource.php

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
* @phpstan-type ResourceData array{
2323
* uri: string,
2424
* name: string,
25+
* title?: string,
2526
* description?: string,
2627
* mimeType?: string,
2728
* annotations?: AnnotationsData,
@@ -47,19 +48,19 @@ class Resource implements \JsonSerializable
4748

4849
/**
4950
* @param string $uri the URI of this resource
50-
* @param string $name A human-readable name for this resource. This can be used by clients to populate UI elements.
51-
* @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.
51+
* @param string $name a short identifier for this resource
52+
* @param ?string $title optional human-readable title for display in UI
53+
* @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.
5254
* @param ?string $mimeType the MIME type of this resource, if known
5355
* @param ?Annotations $annotations optional annotations for the client
54-
* @param ?int $size The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.
56+
* @param ?int $size the size of the raw resource content, in bytes (before base64 encoding or any tokenization), if known
5557
* @param ?Icon[] $icons optional icons representing the resource
56-
* @param ?array<string, mixed> $meta Optional metadata
57-
*
58-
* This can be used by Hosts to display file sizes and estimate context window usage
58+
* @param ?array<string, mixed> $meta optional metadata
5959
*/
6060
public function __construct(
6161
public readonly string $uri,
6262
public readonly string $name,
63+
public readonly ?string $title = null,
6364
public readonly ?string $description = null,
6465
public readonly ?string $mimeType = null,
6566
public readonly ?Annotations $annotations = null,
@@ -94,6 +95,7 @@ public static function fromArray(array $data): self
9495
return new self(
9596
uri: $data['uri'],
9697
name: $data['name'],
98+
title: isset($data['title']) && \is_string($data['title']) ? $data['title'] : null,
9799
description: $data['description'] ?? null,
98100
mimeType: $data['mimeType'] ?? null,
99101
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
@@ -107,6 +109,7 @@ public static function fromArray(array $data): self
107109
* @return array{
108110
* uri: string,
109111
* name: string,
112+
* title?: string,
110113
* description?: string,
111114
* mimeType?: string,
112115
* annotations?: Annotations,
@@ -121,6 +124,9 @@ public function jsonSerialize(): array
121124
'uri' => $this->uri,
122125
'name' => $this->name,
123126
];
127+
if (null !== $this->title) {
128+
$data['title'] = $this->title;
129+
}
124130
if (null !== $this->description) {
125131
$data['description'] = $this->description;
126132
}

src/Schema/ResourceTemplate.php

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* @phpstan-type ResourceTemplateData array{
2222
* uriTemplate: string,
2323
* name: string,
24+
* title?: string,
2425
* description?: string|null,
2526
* mimeType?: string|null,
2627
* annotations?: AnnotationsData|null,
@@ -44,15 +45,17 @@ class ResourceTemplate implements \JsonSerializable
4445

4546
/**
4647
* @param string $uriTemplate a URI template (according to RFC 6570) that can be used to construct resource URIs
47-
* @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.
48-
* @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.
49-
* @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.
50-
* @param Annotations|null $annotations optional annotations for the client
51-
* @param ?array<string, mixed> $meta Optional metadata
48+
* @param string $name a short identifier for this resource template type
49+
* @param ?string $title optional human-readable title for display in UI
50+
* @param ?string $description a description to help the LLM understand available resources
51+
* @param ?string $mimeType the MIME type for all resources that match this template, if uniform
52+
* @param ?Annotations $annotations optional annotations for the client
53+
* @param ?array<string, mixed> $meta optional metadata
5254
*/
5355
public function __construct(
5456
public readonly string $uriTemplate,
5557
public readonly string $name,
58+
public readonly ?string $title = null,
5659
public readonly ?string $description = null,
5760
public readonly ?string $mimeType = null,
5861
public readonly ?Annotations $annotations = null,
@@ -85,6 +88,7 @@ public static function fromArray(array $data): self
8588
return new self(
8689
uriTemplate: $data['uriTemplate'],
8790
name: $data['name'],
91+
title: isset($data['title']) && \is_string($data['title']) ? $data['title'] : null,
8892
description: $data['description'] ?? null,
8993
mimeType: $data['mimeType'] ?? null,
9094
annotations: isset($data['annotations']) ? Annotations::fromArray($data['annotations']) : null,
@@ -96,6 +100,7 @@ public static function fromArray(array $data): self
96100
* @return array{
97101
* uriTemplate: string,
98102
* name: string,
103+
* title?: string,
99104
* description?: string,
100105
* mimeType?: string,
101106
* annotations?: Annotations,
@@ -108,6 +113,9 @@ public function jsonSerialize(): array
108113
'uriTemplate' => $this->uriTemplate,
109114
'name' => $this->name,
110115
];
116+
if (null !== $this->title) {
117+
$data['title'] = $this->title;
118+
}
111119
if (null !== $this->description) {
112120
$data['description'] = $this->description;
113121
}

tests/Unit/Schema/ResourceTemplateTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,45 @@ public function testFromArrayValid(): void
7575
$this->assertInstanceOf(ResourceTemplate::class, $resource);
7676
$this->assertSame(self::VALID_URI, $resource->uriTemplate);
7777
$this->assertSame('list-books', $resource->name);
78+
$this->assertNull($resource->title);
7879
$this->assertNull($resource->description);
7980
$this->assertNull($resource->meta);
8081
}
8182

83+
public function testTitleFromArray(): void
84+
{
85+
$resource = ResourceTemplate::fromArray([
86+
'uriTemplate' => self::VALID_URI,
87+
'name' => 'list-books',
88+
'title' => 'Book Listing',
89+
]);
90+
91+
$this->assertSame('Book Listing', $resource->title);
92+
}
93+
94+
public function testTitleSerialization(): void
95+
{
96+
$resource = new ResourceTemplate(
97+
uriTemplate: self::VALID_URI,
98+
name: 'list-books',
99+
title: 'Book Listing',
100+
);
101+
102+
$data = $resource->jsonSerialize();
103+
$this->assertSame('Book Listing', $data['title']);
104+
}
105+
106+
public function testTitleOmittedWhenNull(): void
107+
{
108+
$resource = new ResourceTemplate(
109+
uriTemplate: self::VALID_URI,
110+
name: 'list-books',
111+
);
112+
113+
$data = $resource->jsonSerialize();
114+
$this->assertArrayNotHasKey('title', $data);
115+
}
116+
82117
#[DataProvider('provideInvalidResources')]
83118
public function testFromArrayInvalid(array $input, string $expectedExceptionMessage): void
84119
{

tests/Unit/Schema/ResourceTest.php

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,45 @@ public function testFromArrayValid(): void
7777
$this->assertInstanceOf(Resource::class, $resource);
7878
$this->assertSame(self::VALID_URI, $resource->uri);
7979
$this->assertSame('list-books', $resource->name);
80+
$this->assertNull($resource->title);
8081
$this->assertNull($resource->description);
8182
$this->assertNull($resource->meta);
8283
}
8384

85+
public function testTitleFromArray(): void
86+
{
87+
$resource = Resource::fromArray([
88+
'uri' => self::VALID_URI,
89+
'name' => 'list-books',
90+
'title' => 'Book Listing',
91+
]);
92+
93+
$this->assertSame('Book Listing', $resource->title);
94+
}
95+
96+
public function testTitleSerialization(): void
97+
{
98+
$resource = new Resource(
99+
uri: self::VALID_URI,
100+
name: 'list-books',
101+
title: 'Book Listing',
102+
);
103+
104+
$data = $resource->jsonSerialize();
105+
$this->assertSame('Book Listing', $data['title']);
106+
}
107+
108+
public function testTitleOmittedWhenNull(): void
109+
{
110+
$resource = new Resource(
111+
uri: self::VALID_URI,
112+
name: 'list-books',
113+
);
114+
115+
$data = $resource->jsonSerialize();
116+
$this->assertArrayNotHasKey('title', $data);
117+
}
118+
84119
#[DataProvider('provideInvalidResources')]
85120
public function testFromArrayInvalid(array $input, string $expectedExceptionMessage): void
86121
{

0 commit comments

Comments
 (0)