Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ All notable changes to `mcp/sdk` will be documented in this file.
* 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.
* Add optional `title` field to `Tool` and `McpTool` for MCP spec compliance
* [BC Break] `Tool::__construct()` signature changed — `$title` parameter added between `$name` and `$inputSchema`. Callers using positional arguments must switch to named arguments or pass `null` for `$title`.
* [BC Break] `McpTool` attribute signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.
* [BC Break] `Builder::addTool()` signature changed — `$title` parameter added between `$name` and `$description`. Callers using positional arguments for `$description` must switch to named arguments.

0.4.0
-----
Expand Down
3 changes: 2 additions & 1 deletion docs/server-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ $server = Server::builder()

- `handler` (callable|string): The tool handler
- `name` (string|null): Optional tool name
- `title` (string|null): Optional human-readable title for display in UI
- `description` (string|null): Optional tool description
- `annotations` (ToolAnnotations|null): Optional annotations for the tool
- `inputSchema` (array|null): Optional input schema for the tool
Expand Down Expand Up @@ -578,7 +579,7 @@ $server = Server::builder()
| `addRequestHandlers()` | handlers | Prepend multiple custom request handlers |
| `addNotificationHandler()` | handler | Prepend a single custom notification handler |
| `addNotificationHandlers()` | handlers | Prepend multiple custom notification handlers |
| `addTool()` | handler, name?, description?, annotations?, inputSchema? | Register tool |
| `addTool()` | handler, name?, title?, description?, annotations?, inputSchema?, ... | Register tool |
| `addResource()` | handler, uri, name?, description?, mimeType?, size?, annotations? | Register resource |
| `addResourceTemplate()` | handler, uriTemplate, name?, description?, mimeType?, annotations? | Register resource template |
| `addPrompt()` | handler, name?, description? | Register prompt |
Expand Down
2 changes: 1 addition & 1 deletion docs/server-client-communication.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use Mcp\Server\RequestContext;

class MyService
{
#[McpTool('my_tool', 'My Tool Description')]
#[McpTool(name: 'my_tool', description: 'My Tool Description')]
public function myTool(RequestContext $context): string
{
$context->getClientGateway()->log(...);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function __construct(
/**
* @return array{incident: string, recommended_actions: string, model: string}
*/
#[McpTool('coordinate_incident_response', 'Coordinate an incident response with logging, progress, and sampling.')]
#[McpTool(name: 'coordinate_incident_response', description: 'Coordinate an incident response with logging, progress, and sampling.')]
public function coordinateIncident(RequestContext $context, string $incidentTitle): array
{
$clientGateway = $context->getClientGateway();
Expand Down
2 changes: 2 additions & 0 deletions examples/server/custom-method-handlers/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
$toolDefinitions = [
'say_hello' => new Tool(
name: 'say_hello',
title: null,
inputSchema: [
'type' => 'object',
'properties' => [
Expand All @@ -37,6 +38,7 @@
),
'sum' => new Tool(
name: 'sum',
title: null,
inputSchema: [
'type' => 'object',
'properties' => [
Expand Down
6 changes: 3 additions & 3 deletions examples/server/elicitation/ElicitationHandlers.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public function __construct(
*
* @return array{status: string, message: string, booking?: array{party_size: int, date: string, dietary: string}}
*/
#[McpTool('book_restaurant', 'Book a restaurant reservation, collecting details via elicitation.')]
#[McpTool(name: 'book_restaurant', description: 'Book a restaurant reservation, collecting details via elicitation.')]
public function bookRestaurant(RequestContext $context, string $restaurantName): array
{
if (!$context->getClientGateway()->supportsElicitation()) {
Expand Down Expand Up @@ -158,7 +158,7 @@ enumNames: ['None', 'Vegetarian', 'Vegan', 'Gluten-Free', 'Halal', 'Kosher'],
*
* @return array{status: string, message: string}
*/
#[McpTool('confirm_action', 'Request user confirmation before proceeding with an action.')]
#[McpTool(name: 'confirm_action', description: 'Request user confirmation before proceeding with an action.')]
public function confirmAction(RequestContext $context, string $actionDescription): array
{
if (!$context->getClientGateway()->supportsElicitation()) {
Expand Down Expand Up @@ -226,7 +226,7 @@ public function confirmAction(RequestContext $context, string $actionDescription
*
* @return array{status: string, message: string, feedback?: array{rating: string, comments: string}}
*/
#[McpTool('collect_feedback', 'Collect user feedback via elicitation form.')]
#[McpTool(name: 'collect_feedback', description: 'Collect user feedback via elicitation form.')]
public function collectFeedback(RequestContext $context, string $topic): array
{
if (!$context->getClientGateway()->supportsElicitation()) {
Expand Down
2 changes: 2 additions & 0 deletions src/Capability/Attribute/McpTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class McpTool
{
/**
* @param string|null $name The name of the tool (defaults to the method name)
* @param string|null $title Optional human-readable title for display in UI
* @param string|null $description The description of the tool (defaults to the DocBlock/inferred)
* @param ToolAnnotations|null $annotations Optional annotations describing tool behavior
* @param ?Icon[] $icons Optional list of icon URLs representing the tool
Expand All @@ -30,6 +31,7 @@ class McpTool
*/
public function __construct(
public ?string $name = null,
public ?string $title = null,
public ?string $description = null,
public ?ToolAnnotations $annotations = null,
public ?array $icons = null,
Expand Down
15 changes: 8 additions & 7 deletions src/Capability/Discovery/Discoverer.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,14 @@ private function processMethod(\ReflectionMethod $method, array &$discoveredCoun
$inputSchema = $this->schemaGenerator->generate($method);
$outputSchema = $this->schemaGenerator->generateOutputSchema($method);
$tool = new Tool(
$name,
$inputSchema,
$description,
$instance->annotations,
$instance->icons,
$instance->meta,
$outputSchema,
name: $name,
title: $instance->title,
inputSchema: $inputSchema,
description: $description,
annotations: $instance->annotations,
icons: $instance->icons,
meta: $instance->meta,
outputSchema: $outputSchema,
);
$tools[$name] = new ToolReference($tool, [$className, $methodName], false);
++$discoveredCount['tools'];
Expand Down
2 changes: 2 additions & 0 deletions src/Capability/Registry/Loader/ArrayLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ final class ArrayLoader implements LoaderInterface
* @param array{
* handler: Handler,
* name: ?string,
* title: ?string,
* description: ?string,
* annotations: ?ToolAnnotations,
* icons: ?Icon[],
Expand Down Expand Up @@ -115,6 +116,7 @@ public function load(RegistryInterface $registry): void

$tool = new Tool(
name: $name,
title: $data['title'] ?? null,
inputSchema: $inputSchema,
description: $description,
annotations: $data['annotations'] ?? null,
Expand Down
30 changes: 18 additions & 12 deletions src/Schema/Tool.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* }
* @phpstan-type ToolData array{
* name: string,
* title?: string,
* inputSchema: ToolInputSchema,
* description?: string|null,
* annotations?: ToolAnnotationsData,
Expand All @@ -47,17 +48,19 @@ class Tool implements \JsonSerializable
{
/**
* @param string $name the name of the tool
* @param ?string $title Optional human-readable title for display in UI
* @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool
* @param ?string $description A human-readable description of the tool.
* This can be used by clients to improve the LLM's understanding of
* available tools. It can be thought of like a "hint" to the model.
* @param ToolInputSchema $inputSchema a JSON Schema object (as a PHP array) defining the expected 'arguments' for the tool
* @param ?ToolAnnotations $annotations optional additional tool information
* @param ?Icon[] $icons optional icons representing the tool
* @param ?array<string, mixed> $meta Optional metadata
* @param ToolOutputSchema|null $outputSchema optional JSON Schema object (as a PHP array) defining the expected output structure
*/
public function __construct(
public readonly string $name,
public readonly ?string $title,
public readonly array $inputSchema,
public readonly ?string $description,
public readonly ?ToolAnnotations $annotations,
Expand Down Expand Up @@ -95,19 +98,21 @@ public static function fromArray(array $data): self
}

return new self(
$data['name'],
$inputSchema,
isset($data['description']) && \is_string($data['description']) ? $data['description'] : null,
isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null,
isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null,
$outputSchema,
name: $data['name'],
title: isset($data['title']) && \is_string($data['title']) ? $data['title'] : null,
inputSchema: $inputSchema,
description: isset($data['description']) && \is_string($data['description']) ? $data['description'] : null,
annotations: isset($data['annotations']) && \is_array($data['annotations']) ? ToolAnnotations::fromArray($data['annotations']) : null,
icons: isset($data['icons']) && \is_array($data['icons']) ? array_map(Icon::fromArray(...), $data['icons']) : null,
meta: isset($data['_meta']) && \is_array($data['_meta']) ? $data['_meta'] : null,
outputSchema: $outputSchema,
);
}

/**
* @return array{
* name: string,
* title?: string,
* inputSchema: ToolInputSchema,
* description?: string,
* annotations?: ToolAnnotations,
Expand All @@ -118,10 +123,11 @@ public static function fromArray(array $data): self
*/
public function jsonSerialize(): array
{
$data = [
'name' => $this->name,
'inputSchema' => $this->inputSchema,
];
$data = ['name' => $this->name];
if (null !== $this->title) {
$data['title'] = $this->title;
}
$data['inputSchema'] = $this->inputSchema;
if (null !== $this->description) {
$data['description'] = $this->description;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/ToolAnnotations.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
class ToolAnnotations implements \JsonSerializable
{
/**
* @param ?string $title a human-readable title for the tool
* @param ?string $title a human-readable title for the tool — deprecated for display in favor of `Mcp\Schema\Tool::$title` per MCP spec revision 2025-06-18; retained for backward compatibility
* @param ?bool $readOnlyHint if true, the tool does not modify its environment
* @param ?bool $destructiveHint If true, the tool may perform destructive updates to its environment. If false, the tool performs only additive updates.
* @param ?bool $idempotentHint If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment. (This property is meaningful only when `readOnlyHint == false`)
Expand Down
4 changes: 4 additions & 0 deletions src/Server/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ final class Builder
* @var array{
* handler: Handler,
* name: ?string,
* title: ?string,
* description: ?string,
* annotations: ?ToolAnnotations,
* icons: ?Icon[],
Expand Down Expand Up @@ -373,6 +374,7 @@ public function setProtocolVersion(ProtocolVersion $protocolVersion): self
* Manually registers a tool handler.
*
* @param Handler $handler
* @param ?string $title Optional human-readable title for display in UI
* @param array<string, mixed>|null $inputSchema
* @param ?Icon[] $icons
* @param array<string, mixed>|null $meta
Expand All @@ -381,6 +383,7 @@ public function setProtocolVersion(ProtocolVersion $protocolVersion): self
public function addTool(
callable|array|string $handler,
?string $name = null,
?string $title = null,
?string $description = null,
?ToolAnnotations $annotations = null,
?array $inputSchema = null,
Expand All @@ -391,6 +394,7 @@ public function addTool(
$this->tools[] = compact(
'handler',
'name',
'title',
'description',
'annotations',
'inputSchema',
Expand Down
24 changes: 12 additions & 12 deletions tests/Conformance/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,18 @@
->setSession(new FileSessionStore(__DIR__.'/sessions'))
->setLogger($logger)
// Tools
->addTool(static fn () => 'This is a simple text response for testing.', 'test_simple_text', 'Tests simple text content response')
->addTool(static fn () => new ImageContent(Elements::TEST_IMAGE_BASE64, 'image/png'), 'test_image_content', 'Tests image content response')
->addTool(static fn () => new AudioContent(Elements::TEST_AUDIO_BASE64, 'audio/wav'), 'test_audio_content', 'Tests audio content response')
->addTool(static fn () => EmbeddedResource::fromText('test://embedded-resource', 'This is an embedded resource content.'), 'test_embedded_resource', 'Tests embedded resource content response')
->addTool([Elements::class, 'toolMultipleTypes'], 'test_multiple_content_types', 'Tests response with multiple content types')
->addTool([Elements::class, 'toolWithLogging'], 'test_tool_with_logging', 'Tests tool that emits log messages')
->addTool([Elements::class, 'toolWithProgress'], 'test_tool_with_progress', 'Tests tool that reports progress notifications')
->addTool([Elements::class, 'toolWithSampling'], 'test_sampling', 'Tests server-initiated sampling')
->addTool(static fn () => CallToolResult::error([new TextContent('This tool intentionally returns an error for testing')]), 'test_error_handling', 'Tests error response handling')
->addTool([Elements::class, 'toolWithElicitation'], 'test_elicitation', 'Tests server-initiated elicitation')
->addTool([Elements::class, 'toolWithElicitationDefaults'], 'test_elicitation_sep1034_defaults', 'Tests elicitation with default values')
->addTool([Elements::class, 'toolWithElicitationEnums'], 'test_elicitation_sep1330_enums', 'Tests elicitation with enum schemas')
->addTool(static fn () => 'This is a simple text response for testing.', name: 'test_simple_text', description: 'Tests simple text content response')
->addTool(static fn () => new ImageContent(Elements::TEST_IMAGE_BASE64, 'image/png'), name: 'test_image_content', description: 'Tests image content response')
->addTool(static fn () => new AudioContent(Elements::TEST_AUDIO_BASE64, 'audio/wav'), name: 'test_audio_content', description: 'Tests audio content response')
->addTool(static fn () => EmbeddedResource::fromText('test://embedded-resource', 'This is an embedded resource content.'), name: 'test_embedded_resource', description: 'Tests embedded resource content response')
->addTool([Elements::class, 'toolMultipleTypes'], name: 'test_multiple_content_types', description: 'Tests response with multiple content types')
->addTool([Elements::class, 'toolWithLogging'], name: 'test_tool_with_logging', description: 'Tests tool that emits log messages')
->addTool([Elements::class, 'toolWithProgress'], name: 'test_tool_with_progress', description: 'Tests tool that reports progress notifications')
->addTool([Elements::class, 'toolWithSampling'], name: 'test_sampling', description: 'Tests server-initiated sampling')
->addTool(static fn () => CallToolResult::error([new TextContent('This tool intentionally returns an error for testing')]), name: 'test_error_handling', description: 'Tests error response handling')
->addTool([Elements::class, 'toolWithElicitation'], name: 'test_elicitation', description: 'Tests server-initiated elicitation')
->addTool([Elements::class, 'toolWithElicitationDefaults'], name: 'test_elicitation_sep1034_defaults', description: 'Tests elicitation with default values')
->addTool([Elements::class, 'toolWithElicitationEnums'], name: 'test_elicitation_sep1330_enums', description: 'Tests elicitation with enum schemas')
// Resources
->addResource(static fn () => 'This is the content of the static text resource.', 'test://static-text', 'static-text', 'A static text resource for testing')
->addResource(static fn () => fopen('data://image/png;base64,'.Elements::TEST_IMAGE_BASE64, 'r'), 'test://static-binary', 'static-binary', 'A static binary resource (image) for testing')
Expand Down
15 changes: 15 additions & 0 deletions tests/Unit/Capability/Attribute/McpToolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ public function testInstantiatesWithMissingOptionalArguments(): void
$this->assertNull($attribute->outputSchema);
}

public function testInstantiatesWithTitle(): void
{
$attribute = new McpTool(name: 'n', title: 'T');

$this->assertSame('n', $attribute->name);
$this->assertSame('T', $attribute->title);
}

public function testDefaultTitleIsNull(): void
{
$attribute = new McpTool();

$this->assertNull($attribute->title);
}

public function testInstantiatesWithOutputSchema(): void
{
// Arrange
Expand Down
33 changes: 33 additions & 0 deletions tests/Unit/Capability/Discovery/DiscovererToolTitleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Mcp\Tests\Unit\Capability\Discovery;

use Mcp\Capability\Discovery\Discoverer;
use Mcp\Capability\Registry\ToolReference;
use PHPUnit\Framework\TestCase;

class DiscovererToolTitleTest extends TestCase
{
public function testDiscoveryPropagatesMcpToolTitleToToolTitle(): void
{
$discoverer = new Discoverer();

$discovery = $discoverer->discover(__DIR__, ['Fixtures']);

$tools = $discovery->getTools();

$this->assertArrayHasKey('greet_user', $tools);
$toolRef = $tools['greet_user'];
$this->assertInstanceOf(ToolReference::class, $toolRef);
$this->assertSame('Greet User', $toolRef->tool->title);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class DiscoverableToolHandler
*
* @return string the greeting
*/
#[McpTool(name: 'greet_user', description: 'Greets a user by name.')]
#[McpTool(name: 'greet_user', description: 'Greets a user by name.', title: 'Greet User')]
public function greet(string $name): string
{
return "Hello, {$name}!";
Expand Down
Loading