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
This commit is contained in:
Christopher Hertel 2026-04-14 10:04:35 +02:00 • committed by GitHub
commit 7af5a345c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 21 additions and 6 deletions

View file

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

View file

@ -352,6 +352,7 @@ $server = Server::builder()
- `handler` (callable|string): The prompt handler - `handler` (callable|string): The prompt handler
- `name` (string|null): Optional prompt name - `name` (string|null): Optional prompt name
- `title` (string|null): Optional human-readable title for display in UI
- `description` (string|null): Optional prompt description - `description` (string|null): Optional prompt description
- `icons` (Icon[]|null): Optional array of icons for the prompt - `icons` (Icon[]|null): Optional array of icons for the prompt

View file

@ -24,12 +24,14 @@ class McpPrompt
{ {
/** /**
* @param ?string $name overrides the prompt name (defaults to method name) * @param ?string $name overrides the prompt name (defaults to method name)
* @param ?string $title Optional human-readable title for display in UI
* @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary. * @param ?string $description Optional description of the prompt. Defaults to method DocBlock summary.
* @param ?Icon[] $icons Optional list of icon URLs representing the prompt * @param ?Icon[] $icons Optional list of icon URLs representing the prompt
* @param ?array<string, mixed> $meta Optional metadata * @param ?array<string, mixed> $meta Optional metadata
*/ */
public function __construct( public function __construct(
public ?string $name = null, public ?string $name = null,
public ?string $title = null,
public ?string $description = null, public ?string $description = null,
public ?array $icons = null, public ?array $icons = null,
public ?array $meta = null, public ?array $meta = null,

View file

@ -276,7 +276,7 @@ final class Discoverer implements DiscovererInterface
$paramTag = $paramTags['$'.$param->getName()] ?? null; $paramTag = $paramTags['$'.$param->getName()] ?? null;
$arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable()); $arguments[] = new PromptArgument($param->getName(), $paramTag ? trim((string) $paramTag->getDescription()) : null, !$param->isOptional() && !$param->isDefaultValueAvailable());
} }
$prompt = new Prompt($name, $description, $arguments, $instance->icons, $instance->meta); $prompt = new Prompt($name, $instance->title, $description, $arguments, $instance->icons, $instance->meta);
$completionProviders = $this->getCompletionProviders($method); $completionProviders = $this->getCompletionProviders($method);
$prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders); $prompts[$name] = new PromptReference($prompt, [$className, $methodName], false, $completionProviders);
++$discoveredCount['prompts']; ++$discoveredCount['prompts'];

View file

@ -252,6 +252,7 @@ final class ArrayLoader implements LoaderInterface
} }
$prompt = new Prompt( $prompt = new Prompt(
name: $name, name: $name,
title: $data['title'] ?? null,
description: $description, description: $description,
arguments: $arguments, arguments: $arguments,
icons: $data['icons'] ?? null, icons: $data['icons'] ?? null,

View file

@ -21,6 +21,7 @@ use Mcp\Exception\InvalidArgumentException;
* *
* @phpstan-type PromptData array{ * @phpstan-type PromptData array{
* name: string, * name: string,
* title?: string,
* description?: string, * description?: string,
* arguments?: PromptArgumentData[], * arguments?: PromptArgumentData[],
* icons?: IconData[], * icons?: IconData[],
@ -33,6 +34,7 @@ class Prompt implements \JsonSerializable
{ {
/** /**
* @param string $name the name of the prompt or prompt template * @param string $name the name of the prompt or prompt template
* @param ?string $title Optional human-readable title for display in UI
* @param ?string $description an optional description of what this prompt provides * @param ?string $description an optional description of what this prompt provides
* @param ?PromptArgument[] $arguments A list of arguments for templating. Null if not a template. * @param ?PromptArgument[] $arguments A list of arguments for templating. Null if not a template.
* @param ?Icon[] $icons optional icons representing the prompt * @param ?Icon[] $icons optional icons representing the prompt
@ -40,6 +42,7 @@ class Prompt implements \JsonSerializable
*/ */
public function __construct( public function __construct(
public readonly string $name, public readonly string $name,
public readonly ?string $title = null,
public readonly ?string $description = null, public readonly ?string $description = null,
public readonly ?array $arguments = null, public readonly ?array $arguments = null,
public readonly ?array $icons = null, public readonly ?array $icons = null,
@ -73,6 +76,7 @@ class Prompt implements \JsonSerializable
return new self( return new self(
name: $data['name'], name: $data['name'],
title: $data['title'] ?? null,
description: $data['description'] ?? null, description: $data['description'] ?? null,
arguments: $arguments, arguments: $arguments,
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null, icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
@ -83,6 +87,7 @@ class Prompt implements \JsonSerializable
/** /**
* @return array{ * @return array{
* name: string, * name: string,
* title?: string,
* description?: string, * description?: string,
* arguments?: array<PromptArgument>, * arguments?: array<PromptArgument>,
* icons?: Icon[], * icons?: Icon[],
@ -92,6 +97,9 @@ class Prompt implements \JsonSerializable
public function jsonSerialize(): array public function jsonSerialize(): array
{ {
$data = ['name' => $this->name]; $data = ['name' => $this->name];
if (null !== $this->title) {
$data['title'] = $this->title;
}
if (null !== $this->description) { if (null !== $this->description) {
$data['description'] = $this->description; $data['description'] = $this->description;
} }

View file

@ -473,11 +473,12 @@ final class Builder
public function addPrompt( public function addPrompt(
\Closure|array|string $handler, \Closure|array|string $handler,
?string $name = null, ?string $name = null,
?string $title = null,
?string $description = null, ?string $description = null,
?array $icons = null, ?array $icons = null,
?array $meta = null, ?array $meta = null,
): self { ): self {
$this->prompts[] = compact('handler', 'name', 'description', 'icons', 'meta'); $this->prompts[] = compact('handler', 'name', 'title', 'description', 'icons', 'meta');
return $this; return $this;
} }

View file

@ -58,10 +58,10 @@ $server = Server::builder()
->addResourceTemplate([Elements::class, 'resourceTemplate'], 'test://template/{id}/data', 'template', 'A resource template with parameter substitution', 'application/json') ->addResourceTemplate([Elements::class, 'resourceTemplate'], 'test://template/{id}/data', 'template', 'A resource template with parameter substitution', 'application/json')
->addResource(static fn () => 'Watched resource content', 'test://watched-resource', 'watched-resource', 'A resource that can be watched') ->addResource(static fn () => 'Watched resource content', 'test://watched-resource', 'watched-resource', 'A resource that can be watched')
// Prompts // Prompts
->addPrompt(static fn () => [['role' => 'user', 'content' => 'This is a simple prompt for testing.']], 'test_simple_prompt', 'A simple prompt without arguments') ->addPrompt(static fn () => [['role' => 'user', 'content' => 'This is a simple prompt for testing.']], name: 'test_simple_prompt', description: 'A simple prompt without arguments')
->addPrompt([Elements::class, 'promptWithArguments'], 'test_prompt_with_arguments', 'A prompt with required arguments') ->addPrompt([Elements::class, 'promptWithArguments'], name: 'test_prompt_with_arguments', description: 'A prompt with required arguments')
->addPrompt([Elements::class, 'promptWithEmbeddedResource'], 'test_prompt_with_embedded_resource', 'A prompt that includes an embedded resource') ->addPrompt([Elements::class, 'promptWithEmbeddedResource'], name: 'test_prompt_with_embedded_resource', description: 'A prompt that includes an embedded resource')
->addPrompt([Elements::class, 'promptWithImage'], 'test_prompt_with_image', 'A prompt that includes image content') ->addPrompt([Elements::class, 'promptWithImage'], name: 'test_prompt_with_image', description: 'A prompt that includes image content')
->build(); ->build();
$response = $server->run($transport); $response = $server->run($transport);