Skip to content

Commit d900068

Browse files
committed
improve mcp docs
1 parent 8dc8c8d commit d900068

File tree

1 file changed

+182
-24
lines changed

1 file changed

+182
-24
lines changed

core/mcp.md

Lines changed: 182 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ MCP defines a standard way for AI models to discover available tools, understand
77
schemas, and invoke them. API Platform leverages its existing metadata system — state processors,
88
validation, serialization — to turn your PHP classes into MCP-compliant tool definitions.
99

10+
> **Note:** The MCP integration is marked `@experimental`. The API may change between minor releases.
11+
1012
## Installation
1113

1214
Install the [MCP Bundle](https://github.com/symfony-tools/mcp-bundle):
@@ -35,6 +37,20 @@ mcp:
3537
ttl: 3600
3638
```
3739
40+
You can also configure API Platform's MCP integration in `api_platform.yaml`:
41+
42+
```yaml
43+
# config/packages/api_platform.yaml
44+
api_platform:
45+
mcp:
46+
enabled: true # default: true
47+
format: jsonld # default: 'jsonld'
48+
```
49+
50+
The `format` option sets the serialization format used for MCP tool structured content. It must be a
51+
format registered in `api_platform.formats` (e.g. `jsonld`, `json`, `jsonapi`). The default `jsonld`
52+
produces rich semantic output with `@context`, `@id`, and `@type` fields.
53+
3854
### Laravel
3955

4056
MCP is enabled by default in the Laravel configuration:
@@ -51,6 +67,16 @@ return [
5167

5268
The MCP endpoint is automatically registered at `/mcp`.
5369

70+
## Architecture
71+
72+
API Platform registers its own `Handler` with the MCP SDK. The handler's `supports()` method returns
73+
`true` only for tools and resources that are registered through API Platform metadata. If a requested
74+
tool or resource is not found in the API Platform registry, the handler returns `false` and the MCP
75+
SDK proceeds through its own handler chain.
76+
77+
This means you can register both API Platform MCP tools and native `mcp/sdk` tools on the same
78+
server — they coexist without conflict.
79+
5480
## Declaring MCP Tools
5581

5682
MCP tools let AI agents invoke operations on your API. The primary pattern uses `#[McpTool]` as a
@@ -179,6 +205,44 @@ class SearchBooksProcessor implements ProcessorInterface
179205
}
180206
```
181207

208+
### Returning a Collection with McpToolCollection
209+
210+
Use `McpToolCollection` instead of `McpTool` when a tool returns a collection of items. It extends
211+
`McpTool` and implements `CollectionOperationInterface`, which signals to the serializer and schema
212+
factory that the output is a list.
213+
214+
```php
215+
<?php
216+
namespace App\ApiResource;
217+
218+
use ApiPlatform\Metadata\ApiResource;
219+
use ApiPlatform\Metadata\McpToolCollection;
220+
use App\Dto\SearchQuery;
221+
use App\State\SearchBooksProcessor;
222+
223+
#[ApiResource(
224+
operations: [],
225+
mcp: [
226+
'list_books' => new McpToolCollection(
227+
description: 'List Books',
228+
input: SearchQuery::class,
229+
processor: SearchBooksProcessor::class,
230+
structuredContent: true,
231+
),
232+
]
233+
)]
234+
class Book
235+
{
236+
public ?int $id = null;
237+
public ?string $title = null;
238+
public ?string $isbn = null;
239+
}
240+
```
241+
242+
When `structuredContent: true`, the structured content response includes `@context`,
243+
`hydra:totalItems`, and a `hydra:member` array containing the serialized items. The output schema
244+
published to MCP clients reflects this collection structure.
245+
182246
### Returning Custom Results
183247

184248
By default, tool results are serialized using API Platform's [serialization](serialization.md)
@@ -223,9 +287,96 @@ class Report
223287
Setting `structuredContent: false` disables the automatic JSON serialization. When returning a
224288
`CallToolResult`, the response is sent as-is to the AI agent.
225289

290+
## Using McpTool with ApiResource
291+
292+
The standalone `#[McpTool]` class attribute is convenient for simple tools, but you can also declare
293+
MCP tools inside an `#[ApiResource]` attribute using the `mcp` parameter. This is the appropriate
294+
pattern when:
295+
296+
- The class should not expose any HTTP endpoints (`operations: []`)
297+
- You need to combine multiple MCP operations on a single class
298+
- You need fine-grained control that is cleaner to express at the resource level
299+
300+
The `mcp` parameter accepts an associative array of `McpTool` or `McpResource` instances, keyed by
301+
the operation name. The array key is the tool name — the `name` parameter inside `new McpTool(...)`
302+
is redundant when using this pattern and should be omitted. Setting `operations: []` means no HTTP
303+
routes are registered for the class — it exists solely as an MCP tool definition.
304+
305+
### Simple Tool with a Dedicated Processor
306+
307+
```php
308+
<?php
309+
// api/src/ApiResource/ReadHydraResource.php with Symfony or app/ApiResource/ReadHydraResource.php with Laravel
310+
namespace App\ApiResource;
311+
312+
use ApiPlatform\Metadata\ApiResource;
313+
use ApiPlatform\Metadata\McpTool;
314+
use App\State\ReadHydraResourceProcessor;
315+
316+
#[ApiResource(
317+
operations: [],
318+
mcp: [
319+
'read_hydra_resource' => new McpTool(
320+
description: 'Navigate to a Hydra API resource by URI.',
321+
processor: ReadHydraResourceProcessor::class,
322+
structuredContent: false,
323+
),
324+
],
325+
)]
326+
class ReadHydraResource
327+
{
328+
public string $uri;
329+
}
330+
```
331+
332+
The class properties define the tool's `inputSchema`. The processor receives a `ReadHydraResource`
333+
instance and returns the result. Because `structuredContent: false` is set, the processor can return
334+
a `CallToolResult` directly, bypassing automatic JSON serialization.
335+
336+
### Customizing Property Schemas with ApiProperty
337+
338+
Some LLM providers reject JSON Schema union types such as `["array", "null"]` that PHP nullable
339+
types produce by default. Use `#[ApiProperty(schema: [...])]` to override the generated schema for
340+
a specific property:
341+
342+
```php
343+
<?php
344+
// api/src/ApiResource/InvokeHydraOperation.php with Symfony or app/ApiResource/InvokeHydraOperation.php with Laravel
345+
namespace App\ApiResource;
346+
347+
use ApiPlatform\Metadata\ApiProperty;
348+
use ApiPlatform\Metadata\ApiResource;
349+
use ApiPlatform\Metadata\McpTool;
350+
use App\State\InvokeHydraOperationProcessor;
351+
352+
#[ApiResource(
353+
operations: [],
354+
mcp: [
355+
'invoke_hydra_operation' => new McpTool(
356+
description: 'Execute a state-changing operation on a Hydra API resource.',
357+
processor: InvokeHydraOperationProcessor::class,
358+
structuredContent: false,
359+
),
360+
],
361+
)]
362+
class InvokeHydraOperation
363+
{
364+
public string $uri;
365+
public string $method;
366+
#[ApiProperty(schema: ['type' => 'object', 'description' => 'JSON payload for the request'])]
367+
public ?array $payload = null;
368+
}
369+
```
370+
371+
Without the `#[ApiProperty]` override, `?array $payload` would generate `{"type": ["array", "null"]}`.
372+
The explicit schema replaces it with `{"type": "object", ...}`, which all major LLM providers accept.
373+
226374
## Validation
227375

228-
MCP tool inputs support validation using the same mechanisms as regular API Platform operations.
376+
The MCP SDK already validates tool inputs against the JSON Schema at the transport level (types,
377+
required fields, etc.). API Platform's own validation pipeline is disabled by default for MCP tools.
378+
Set `validate: true` to enable business-level validation — constraints like email format, string
379+
length, or custom rules that go beyond structural schema checks.
229380

230381
On Symfony, use [Symfony Validator constraints](../symfony/validation.md):
231382

@@ -239,7 +390,8 @@ use Symfony\Component\Validator\Constraints as Assert;
239390
#[McpTool(
240391
name: 'submit_contact',
241392
description: 'Submit a contact form',
242-
processor: [self::class, 'process']
393+
processor: [self::class, 'process'],
394+
validate: true // Must be explicitly enabled for MCP tools
243395
)]
244396
class ContactForm
245397
{
@@ -265,6 +417,7 @@ On Laravel, use [validation rules](../laravel/validation.md):
265417
name: 'submit_contact',
266418
description: 'Submit a contact form',
267419
processor: [self::class, 'process'],
420+
validate: true, // Must be explicitly enabled for MCP tools
268421
rules: [
269422
'name' => 'required|min:3|max:50',
270423
'email' => 'required|email',
@@ -321,30 +474,35 @@ The `uri` must be unique across the MCP server and follows the `resource://` URI
321474

322475
The `McpTool` attribute accepts all standard [operation options](operations.md) plus:
323476

324-
| Option | Description |
325-
| ------------------- | ------------------------------------------------------------------------- |
326-
| `name` | Tool name exposed to AI agents (defaults to the class short name) |
327-
| `description` | Human-readable description of the tool (defaults to class DocBlock) |
328-
| `structuredContent` | Whether to include JSON structured content in responses (default: `true`) |
329-
| `input` | A separate DTO class to use as the tool's input schema |
330-
| `output` | A separate DTO class to use as the tool's output representation |
331-
| `annotations` | MCP tool annotations describing behavior hints |
332-
| `icons` | List of icon URLs representing the tool |
333-
| `meta` | Arbitrary metadata |
334-
| `rules` | Laravel validation rules (Laravel only) |
477+
| Option | Description |
478+
| --------------------- | ------------------------------------------------------------------------- |
479+
| `name` | Tool name exposed to AI agents (defaults to the class short name) |
480+
| `description` | Human-readable description of the tool (defaults to class DocBlock) |
481+
| `structuredContent` | Whether to include JSON structured content in responses (default: `true`) |
482+
| `input` | A separate DTO class to use as the tool's input schema |
483+
| `output` | A separate DTO class to use as the tool's output representation |
484+
| `inputFormats` | Serialization formats for deserializing the tool input (e.g. `['json']`) |
485+
| `outputFormats` | Serialization formats for serializing structured content (e.g. `['jsonld']`); MCP uses JSON-RPC as transport, so this controls the internal serialization format only |
486+
| `contentNegotiation` | Whether to enable HTTP content negotiation (default: `false` for MCP; set to `true` only if you need format negotiation via Accept headers) |
487+
| `validate` | Whether to run the validation pipeline (default: `false` for MCP; set to `true` to enable) |
488+
| `annotations` | MCP tool annotations describing behavior hints |
489+
| `icons` | List of icon URLs representing the tool |
490+
| `meta` | Arbitrary metadata |
491+
| `rules` | Laravel validation rules (Laravel only) |
335492

336493
## McpResource Options
337494

338495
The `McpResource` attribute accepts all standard [operation options](operations.md) plus:
339496

340-
| Option | Description |
341-
| ------------------- | -------------------------------------------------------------------------- |
342-
| `uri` | Unique URI identifying this resource (required, uses `resource://` scheme) |
343-
| `name` | Human-readable name for the resource |
344-
| `description` | Description of the resource (defaults to class DocBlock) |
345-
| `structuredContent` | Whether to include JSON structured content (default: `true`) |
346-
| `mimeType` | MIME type of the resource content |
347-
| `size` | Size in bytes, if known |
348-
| `annotations` | MCP resource annotations |
349-
| `icons` | List of icon URLs |
350-
| `meta` | Arbitrary metadata |
497+
| Option | Description |
498+
| --------------------- | -------------------------------------------------------------------------- |
499+
| `uri` | Unique URI identifying this resource (required, uses `resource://` scheme) |
500+
| `name` | Human-readable name for the resource |
501+
| `description` | Description of the resource (defaults to class DocBlock) |
502+
| `structuredContent` | Whether to include JSON structured content (default: `true`) |
503+
| `contentNegotiation` | Whether to enable HTTP content negotiation (default: `false` for MCP; set to `true` only if you need format negotiation via Accept headers) |
504+
| `mimeType` | MIME type of the resource content |
505+
| `size` | Size in bytes, if known |
506+
| `annotations` | MCP resource annotations |
507+
| `icons` | List of icon URLs |
508+
| `meta` | Arbitrary metadata |

0 commit comments

Comments
 (0)