@@ -7,6 +7,8 @@ MCP defines a standard way for AI models to discover available tools, understand
77schemas, and invoke them. API Platform leverages its existing metadata system — state processors,
88validation, 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
1214Install the [ MCP Bundle] ( https://github.com/symfony-tools/mcp-bundle ) :
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
4056MCP is enabled by default in the Laravel configuration :
@@ -51,6 +67,16 @@ return [
5167
5268The 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
5682MCP 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\A piResource;
217+
218+ use ApiPlatform\M etadata\A piResource;
219+ use ApiPlatform\M etadata\M cpToolCollection;
220+ use App\D to\S earchQuery;
221+ use App\S tate\S earchBooksProcessor;
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
184248By default, tool results are serialized using API Platform's [serialization](serialization.md)
@@ -223,9 +287,96 @@ class Report
223287Setting `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\A piResource;
311+
312+ use ApiPlatform\M etadata\A piResource;
313+ use ApiPlatform\M etadata\M cpTool;
314+ use App\S tate\R eadHydraResourceProcessor;
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\A piResource;
346+
347+ use ApiPlatform\M etadata\A piProperty;
348+ use ApiPlatform\M etadata\A piResource;
349+ use ApiPlatform\M etadata\M cpTool;
350+ use App\S tate\I nvokeHydraOperationProcessor;
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
230381On 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)]
244396class 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
322475The `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
338495The `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