Skip to content

Commit 7af5a34

Browse files
authored
Add title field to Prompt for MCP spec compliance (#278)
The MCP 2025-11-25 specification defines an optional `title` property on Prompt for human-readable display in UI contexts, distinct from the programmatic `name` identifier. This was missing from the SDK. BC Break: Builder::addPrompt() signature changed — $title parameter added between $name and $description. Callers using positional arguments for $description must switch to named arguments. Closes #276
1 parent 863cfc1 commit 7af5a34

8 files changed

Lines changed: 21 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ All notable changes to `mcp/sdk` will be documented in this file.
1212
* [BC break] Make Symfony Finder component optional. Users would need to install `symfony/finder` now themselves
1313
* Add `LenientOidcDiscoveryMetadataPolicy` for identity providers that omit `code_challenge_methods_supported` (e.g. FusionAuth, Microsoft Entra ID)
1414
* Add OAuth 2.0 Dynamic Client Registration middleware (RFC 7591)
15+
* Add optional `title` field to `Prompt` and `McpPrompt` for MCP spec compliance
16+
* [BC Break] `Builder::addPrompt()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
1517

1618
0.4.0
1719
-----

docs/server-builder.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ $server = Server::builder()
352352

353353
- `handler` (callable|string): The prompt handler
354354
- `name` (string|null): Optional prompt name
355+
- `title` (string|null): Optional human-readable title for display in UI
355356
- `description` (string|null): Optional prompt description
356357
- `icons` (Icon[]|null): Optional array of icons for the prompt
357358

src/Capability/Attribute/McpPrompt.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ class McpPrompt
2424
{
2525
/**
2626
* @param ?string $name overrides the prompt name (defaults to method name)
27+
* @param ?string $title Optional human-readable title for display in UI
2728
* @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary.
2829
* @param ?Icon[] $icons Optional list of icon URLs representing the prompt
2930
* @param ?array<string, mixed> $meta Optional metadata
3031
*/
3132
public function __construct(
3233
public ?string $name = null,
34+
public ?string $title = null,
3335
public ?string $description = null,
3436
public ?array $icons = null,
3537
public ?array $meta = null,

src/Capability/Discovery/Discoverer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
276276
$paramTag = $paramTags['$'.$param->getName()] ?? null;
277277
$arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable());
278278
}
279-
$prompt = new Prompt($name, $description, $arguments, $instance->icons, $instance->meta);
279+
$prompt = new Prompt($name, $instance->title, $description, $arguments, $instance->icons, $instance->meta);
280280
$completionProviders = $this->getCompletionProviders($method);
281281
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders);
282282
++$discoveredCount['prompts'];

src/Capability/Registry/Loader/ArrayLoader.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ public function load(RegistryInterface $registry): void
252252
}
253253
$prompt = new Prompt(
254254
name: $name,
255+
title: $data['title'] ?? null,
255256
description: $description,
256257
arguments: $arguments,
257258
icons: $data['icons'] ?? null,

src/Schema/Prompt.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
*
2222
* @phpstan-type PromptData array{
2323
* name: string,
24+
* title?: string,
2425
* description?: string,
2526
* arguments?: PromptArgumentData[],
2627
* icons?: IconData[],
@@ -33,13 +34,15 @@ class Prompt implements \JsonSerializable
3334
{
3435
/**
3536
* @param string $name the name of the prompt or prompt template
37+
* @param ?string $title Optional human-readable title for display in UI
3638
* @param ?string $description an optional description of what this prompt provides
3739
* @param ?PromptArgument[] $arguments A list of arguments for templating. Null if not a template.
3840
* @param ?Icon[] $icons optional icons representing the prompt
3941
* @param ?array<string, mixed> $meta Optional metadata
4042
*/
4143
public function __construct(
4244
public readonly string $name,
45+
public readonly ?string $title = null,
4346
public readonly ?string $description = null,
4447
public readonly ?array $arguments = null,
4548
public readonly ?array $icons = null,
@@ -73,6 +76,7 @@ public static function fromArray(array $data): self
7376

7477
return new self(
7578
name: $data['name'],
79+
title: $data['title'] ?? null,
7680
description: $data['description'] ?? null,
7781
arguments: $arguments,
7882
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
@@ -83,6 +87,7 @@ public static function fromArray(array $data): self
8387
/**
8488
* @return array{
8589
* name: string,
90+
* title?: string,
8691
* description?: string,
8792
* arguments?: array<PromptArgument>,
8893
* icons?: Icon[],
@@ -92,6 +97,9 @@ public static function fromArray(array $data): self
9297
public function jsonSerialize(): array
9398
{
9499
$data = ['name' => $this->name];
100+
if (null !== $this->title) {
101+
$data['title'] = $this->title;
102+
}
95103
if (null !== $this->description) {
96104
$data['description'] = $this->description;
97105
}

src/Server/Builder.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -473,11 +473,12 @@ public function addResourceTemplate(
473473
public function addPrompt(
474474
\Closure|array|string $handler,
475475
?string $name = null,
476+
?string $title = null,
476477
?string $description = null,
477478
?array $icons = null,
478479
?array $meta = null,
479480
): self {
480-
$this->prompts[] = compact('handler', 'name', 'description', 'icons', 'meta');
481+
$this->prompts[] = compact('handler', 'name', 'title', 'description', 'icons', 'meta');
481482

482483
return $this;
483484
}

tests/Conformance/server.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@
5858
->addResourceTemplate([Elements::class, 'resourceTemplate'], 'test://template/{id}/data', 'template', 'A resource template with parameter substitution', 'application/json')
5959
->addResource(static fn () => 'Watched resource content', 'test://watched-resource', 'watched-resource', 'A resource that can be watched')
6060
// Prompts
61-
->addPrompt(static fn () => [['role' => 'user', 'content' => 'This is a simple prompt for testing.']], 'test_simple_prompt', 'A simple prompt without arguments')
62-
->addPrompt([Elements::class, 'promptWithArguments'], 'test_prompt_with_arguments', 'A prompt with required arguments')
63-
->addPrompt([Elements::class, 'promptWithEmbeddedResource'], 'test_prompt_with_embedded_resource', 'A prompt that includes an embedded resource')
64-
->addPrompt([Elements::class, 'promptWithImage'], 'test_prompt_with_image', 'A prompt that includes image content')
61+
->addPrompt(static fn () => [['role' => 'user', 'content' => 'This is a simple prompt for testing.']], name: 'test_simple_prompt', description: 'A simple prompt without arguments')
62+
->addPrompt([Elements::class, 'promptWithArguments'], name: 'test_prompt_with_arguments', description: 'A prompt with required arguments')
63+
->addPrompt([Elements::class, 'promptWithEmbeddedResource'], name: 'test_prompt_with_embedded_resource', description: 'A prompt that includes an embedded resource')
64+
->addPrompt([Elements::class, 'promptWithImage'], name: 'test_prompt_with_image', description: 'A prompt that includes image content')
6565
->build();
6666

6767
$response = $server->run($transport);

0 commit comments

Comments
 (0)