Skip to content

Latest commit

 

History

History
318 lines (252 loc) · 12.5 KB

tools.md

File metadata and controls

318 lines (252 loc) · 12.5 KB

Tools

Tools are callable functions that perform actions and return results.

use Mcp\Server\Capability\Attribute\McpTool;
use Mcp\Server\Exception\ToolCallException;

class Calculator
{
    /**
     * Performs arithmetic operations with validation.
     */
    #[McpTool(name: 'calculate')]
    public function performCalculation(float $a, float $b, string $operation): float
    {
        return match($operation) {
            'add' => $a + $b,
            'subtract' => $a - $b,
            'multiply' => $a * $b,
            'divide' => $b != 0 ? $a / $b : throw new ToolCallException('Division by zero'),
            default => throw new ToolCallException('Invalid operation')
        };
    }
}

Parameters

  • name (optional): Tool identifier. Defaults to the method name. On an invokable class, it defaults to the class short name.
  • title (optional): Human-readable display title shown in client UI. Distinct from name.
  • description (optional): Tool description. Falls back to the docblock (summary plus long description); stays unset if there is no docblock.
  • annotations (optional): ToolAnnotations object for additional metadata.
  • outputSchema (optional): JSON schema describing structuredContent; see Structured Output.
  • icons (optional): Array of Icon objects for visual representation.
  • meta (optional): Arbitrary key-value pairs for custom metadata.

Priority: name is the attribute parameter, else the method name (the class short name for an invokable class). description is the attribute parameter, else the docblock. The method name is never used as a description.

For tool parameter validation and JSON schema generation, see Schema generation.

Tool Return Values

Tools can return any data type and the SDK will automatically wrap them in appropriate MCP content types.

Automatic Content Wrapping

// Primitive types → TextContent
public function getString(): string { return "Hello"; }           // TextContent
public function getNumber(): int { return 42; }                  // TextContent  
public function getBool(): bool { return true; }                 // TextContent
public function getArray(): array { return ['key' => 'value']; } // TextContent (JSON)

// Special cases
public function getNull(): ?string { return null; }              // TextContent("(null)")
public function returnVoid(): void { /* no return */ }           // TextContent("(null)")

Explicit Content Types

For fine control over output formatting:

use Mcp\Schema\Content\{TextContent, ImageContent, AudioContent, ResourceLink, EmbeddedResource, TextResourceContents};

public function getFormattedCode(): TextContent
{
    return TextContent::code('<?php echo "Hello";', 'php');
}

public function getMarkdown(): TextContent  
{
    return new TextContent("# Title\n\nMarkdown content");
}

public function getImage(): ImageContent
{
    return new ImageContent(
        data: base64_encode(file_get_contents('image.png')),
        mimeType: 'image/png'
    );
}

public function getAudio(): AudioContent
{
    return new AudioContent(
        data: base64_encode(file_get_contents('audio.mp3')),
        mimeType: 'audio/mpeg'
    );
}

public function getEmbeddedResource(): EmbeddedResource
{
    return EmbeddedResource::fromText('file://data.json', 'File content');

    // …or build the resource contents yourself:
    // return new EmbeddedResource(
    //     new TextResourceContents('file://data.json', 'application/json', '{}')
    // );
}

public function getResourceLink(): ResourceLink
{
    // Reference a resource by URI without embedding its contents, e.g. when
    // a tool result would otherwise need to inline many or large resources.
    return new ResourceLink(
        uri: 'file://data.json',
        name: 'data.json',
        mimeType: 'application/json'
    );
}

Multiple Content Items

Return an array of content items:

use Mcp\Schema\Content\TextContent;

public function getMultipleContent(): array
{
    return [
        new TextContent('Here is the analysis:'),
        TextContent::code($code, 'php'),
        new TextContent('And here is the summary.')
    ];
}

Structured Output

Besides the human-readable content, a tool result can carry a machine-readable structuredContent value. Declare its shape with outputSchema, a JSON Schema. This example uses an object schema, which works with every revision:

use Mcp\Server\Capability\Attribute\McpTool;

#[McpTool(
    name: 'get_weather',
    outputSchema: [
        'type' => 'object',
        'properties' => [
            'temperature' => ['type' => 'number'],
            'conditions' => ['type' => 'string'],
        ],
        'required' => ['temperature', 'conditions'],
    ]
)]
public function getWeather(string $city): array
{
    // Sent as `structuredContent`, and JSON-encoded into `content` for clients that ignore it
    return ['temperature' => 22.5, 'conditions' => 'sunny'];
}

The same schema can be passed to manual registration:

$builder->addTool([WeatherHandler::class, 'getWeather'], outputSchema: [/* ... */]);

The SDK fills structuredContent whenever the return value qualifies — outputSchema is what tells clients to expect it and lets them validate it. What qualifies depends on the protocol revision the call is served under:

Return value structuredContent
Associative array (['temperature' => 22.5]) The array
Object (stdClass, DTO, JsonSerializable) that serializes to a JSON object Its JSON representation
List ([1, 2, 3], [['id' => 1], ['id' => 2]]), or an object serializing to one Omitted before 2026-07-28, kept from it on
Array holding Content instances Omitted (already carried in content)
Scalar (22.5, 'sunny'), or a JsonSerializable serializing to one Omitted before 2026-07-28. From it on, kept if the tool declares an outputSchema
null, or a JsonSerializable serializing to it Omitted before 2026-07-28. From it on, kept if the tool declares an outputSchema
Content instances Omitted

Up to revision 2025-11-25, structuredContent had to be a JSON object, so a PHP list — which serializes to a JSON array — was not emittable and strict clients rejected the whole tool call over one. SEP-2106, part of revision 2026-07-28, widened outputSchema to any JSON Schema 2020-12 and structuredContent to any JSON value conforming to it. The SDK picks the rule from the revision negotiated for the call, so a tool serving both eras needs the object shape to produce structured output everywhere. Wrap the list in a key for that:

use Mcp\Server\Capability\Attribute\McpTool;

// Structured content only from 2026-07-28 on: a bare list is not a JSON object
public function listUsersFlat(): array
{
    return [['id' => 1], ['id' => 2]];
}

#[McpTool(outputSchema: [
    'type' => 'object',
    'properties' => [
        'items' => ['type' => 'array', 'items' => ['type' => 'object']],
    ],
    'required' => ['items']
])]
public function listUsers(): array
{
    return ['items' => [['id' => 1], ['id' => 2]]];
}

Either way the data reaches the client in content. A scalar sent as structuredContent has its serialized JSON there: 'sunny' gives the text "sunny". A return value with no structured form becomes a TextContent. An array or object is JSON-encoded, a scalar is plain text (sunny) and null is the text (null).

When a tool declares an outputSchema but returns something that cannot be sent as structuredContent, the SDK logs a warning and sends the value in content only.

A tool that wants to branch on the revision itself can read it from the injected RequestContext, see Talking back to the client.

Output validation

The specification states that a server must produce structured results that conform to the declared schema. The SDK holds you to it: when a tool declares an outputSchema and the result carries structuredContent, the SDK validates that value against the schema before sending it. A mismatch becomes a tool error result, so the model reads the reason and can retry:

{
  "content": [
    {
      "type": "text",
      "text": "Invalid structured output for tool 'get_weather': Missing required properties: `temperature`."
    }
  ],
  "isError": true
}

The check applies to a CallToolResult you build yourself as well as to one the SDK wraps for you. It is skipped in three cases:

  • The tool declares no outputSchema.
  • The result carries no structuredContent. If the SDK wrapped your return value, it logs the warning described above. A CallToolResult you build yourself without structuredContent gets no warning.
  • The result is already marked isError: true, because its content is a failure message and not the declared output.

Absent and null structured content

On 2026-07-28, a tool returning null with a nullable outputSchema sends "structuredContent": null. Without an output schema, null is carried only in content. A modern null result that violates the output schema becomes a tool error. A conforming structured null also has the JSON text null in content.

When building a CallToolResult, omit the structuredContent argument to leave the field out; passing structuredContent: null includes an explicit JSON null. success() and error() leave it out. After decoding a result, use hasStructuredContent() to distinguish an absent field from an explicit null; the structuredContent property is null in both cases. For a runtime choice, pass Mcp\Schema\Enum\StructuredContent::Absent to omit the field rather than passing null. Existing calls such as new CallToolResult($content, false, null, $meta) must omit the third argument (use meta: $meta) or pass that sentinel if they intend to send text only.

The stateless-lifecycle example has a tool with a nullable outputSchema, and its client prints all three cases.

Sending only structured content

By default, the SDK also JSON-encodes structured content into content. To send a large result only once, return a CallToolResult with an empty content list:

use Mcp\Server\Capability\Attribute\McpTool;
use Mcp\Schema\Result\CallToolResult;

#[McpTool(outputSchema: [/* ... */])]
public function getWeather(string $city): CallToolResult
{
    return new CallToolResult([], structuredContent: ['temperature' => 22.5, 'conditions' => 'sunny']);
}

The SDK still validates structuredContent against the outputSchema, but it doesn't change the value for older revisions. Before 2026-07-28, clients may reject a list, a scalar or explicit null, so always pass an associative array or an object. An explicit null that fails an object output schema becomes a tool error in every revision.

!!! warning The specification recommends also sending the serialized JSON as text, and SEP-2106 requires it for a list or a scalar. Some hosts pass only content to the model, which then sees an empty result. Send only structured content with object values and for clients that read structuredContent.

Error Handling

Tool handlers can throw any exception, but the type determines how it's handled:

  • ToolCallException: Converted to JSON-RPC response with CallToolResult where isError: true, allowing the LLM to see the error message and self-correct
  • Any other exception you throw from your handler: Converted to JSON-RPC error response, but with a generic error message
use Mcp\Server\Capability\Attribute\McpTool;
use Mcp\Server\Exception\ToolCallException;

#[McpTool]
public function divideNumbers(float $a, float $b): float
{
    if ($b === 0.0) {
        throw new ToolCallException('Division by zero is not allowed');
    }

    return $a / $b;
}

#[McpTool]
public function processFile(string $filename): string
{
    if (!file_exists($filename)) {
        throw new ToolCallException("File not found: {$filename}");
    }

    return file_get_contents($filename);
}

Recommendation: Use ToolCallException when you want to communicate specific errors to clients. Any other exception you throw will still be converted to JSON-RPC compliant errors but with generic error messages.