Skip to content

Commit 9af65a2

Browse files
authored
mcp (#2251)
1 parent 68c3f02 commit 9af65a2

2 files changed

Lines changed: 335 additions & 0 deletions

File tree

core/mcp.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# MCP: Exposing Your API to AI Agents
2+
3+
API Platform integrates with the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) to expose your API as tools and resources that AI agents (LLMs) can discover and interact with.
4+
5+
MCP defines a standard way for AI models to discover available tools, understand their input schemas, and invoke them. API Platform leverages its existing metadata system — state processors, validation, serialization — to turn your PHP classes into MCP-compliant tool definitions.
6+
7+
## Installation
8+
9+
Install the [MCP Bundle](https://github.com/symfony-tools/mcp-bundle):
10+
11+
```console
12+
composer require symfony/mcp-bundle
13+
```
14+
15+
## Configuring the MCP Server
16+
17+
### Symfony
18+
19+
Enable the MCP server and configure the transport in your Symfony configuration:
20+
21+
```yaml
22+
# config/packages/mcp.yaml
23+
mcp:
24+
client_transports:
25+
http: true
26+
stdio: false
27+
http:
28+
path: '/mcp'
29+
session:
30+
store: 'file'
31+
directory: '%kernel.cache_dir%/mcp'
32+
ttl: 3600
33+
```
34+
35+
### Laravel
36+
37+
MCP is enabled by default in the Laravel configuration:
38+
39+
```php
40+
// config/api-platform.php
41+
return [
42+
// ...
43+
'mcp' => [
44+
'enabled' => true,
45+
],
46+
];
47+
```
48+
49+
The MCP endpoint is automatically registered at `/mcp`.
50+
51+
## Declaring MCP Tools
52+
53+
MCP tools let AI agents invoke operations on your API. The primary pattern uses `#[McpTool]` as a class attribute: the class properties define the tool's input schema, and a [state processor](state-processors.md) handles the command.
54+
55+
This follows a CQRS-style approach: tools receive input from AI agents and process it through your application logic.
56+
57+
### Simple Tool
58+
59+
```php
60+
<?php
61+
// api/src/ApiResource/ProcessMessage.php with Symfony or app/ApiResource/ProcessMessage.php with Laravel
62+
namespace App\ApiResource;
63+
64+
use ApiPlatform\Metadata\McpTool;
65+
66+
#[McpTool(
67+
name: 'process_message',
68+
description: 'Process a message with priority',
69+
processor: [self::class, 'process']
70+
)]
71+
class ProcessMessage
72+
{
73+
public function __construct(
74+
private string $message,
75+
private int $priority = 1,
76+
) {}
77+
78+
public function getMessage(): string
79+
{
80+
return $this->message;
81+
}
82+
83+
public function setMessage(string $message): void
84+
{
85+
$this->message = $message;
86+
}
87+
88+
public function getPriority(): int
89+
{
90+
return $this->priority;
91+
}
92+
93+
public function setPriority(int $priority): void
94+
{
95+
$this->priority = $priority;
96+
}
97+
98+
public static function process($data): mixed
99+
{
100+
$data->setMessage('Processed: ' . $data->getMessage());
101+
$data->setPriority($data->getPriority() + 10);
102+
103+
return $data;
104+
}
105+
}
106+
```
107+
108+
The class properties (`$message`, `$priority`) become the tool's `inputSchema`. When an AI agent calls this tool, API Platform deserializes the input into a `ProcessMessage` instance and passes it to the processor. The returned object is serialized back as structured content.
109+
110+
You can also use a [dedicated state processor service](state-processors.md) instead of a static method — any callable or service class implementing `ProcessorInterface` works.
111+
112+
### Using a Separate Input DTO
113+
114+
When the tool's input schema should differ from the class itself, use the `input` option to specify a separate DTO:
115+
116+
```php
117+
<?php
118+
namespace App\Dto;
119+
120+
class SearchQuery
121+
{
122+
public string $search;
123+
}
124+
```
125+
126+
```php
127+
<?php
128+
namespace App\ApiResource;
129+
130+
use ApiPlatform\Metadata\McpTool;
131+
use App\Dto\SearchQuery;
132+
use App\State\SearchBooksProcessor;
133+
134+
#[McpTool(
135+
name: 'search_books',
136+
description: 'Search books by keyword',
137+
input: SearchQuery::class,
138+
processor: SearchBooksProcessor::class,
139+
)]
140+
class BookSearchResult
141+
{
142+
public int $id;
143+
public string $title;
144+
public string $isbn;
145+
}
146+
```
147+
148+
Here, `SearchQuery` defines the tool's `inputSchema` (what the AI agent sends), while `BookSearchResult` defines the output structure. The processor receives a `SearchQuery` instance and returns the result:
149+
150+
```php
151+
<?php
152+
namespace App\State;
153+
154+
use ApiPlatform\Metadata\Operation;
155+
use ApiPlatform\State\ProcessorInterface;
156+
use App\Entity\Book;
157+
use Doctrine\Persistence\ManagerRegistry;
158+
159+
class SearchBooksProcessor implements ProcessorInterface
160+
{
161+
public function __construct(private readonly ManagerRegistry $managerRegistry) {}
162+
163+
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ?iterable
164+
{
165+
return $this->managerRegistry->getRepository(Book::class)->findAll();
166+
}
167+
}
168+
```
169+
170+
### Returning Custom Results
171+
172+
By default, tool results are serialized using API Platform's [serialization](serialization.md) system with structured content (JSON). If you need full control over the response, return a `CallToolResult` directly from your processor and set `structuredContent: false`:
173+
174+
```php
175+
<?php
176+
namespace App\ApiResource;
177+
178+
use ApiPlatform\Metadata\McpTool;
179+
use Mcp\Schema\Content\TextContent;
180+
use Mcp\Schema\Result\CallToolResult;
181+
182+
#[McpTool(
183+
name: 'generate_report',
184+
description: 'Generate a markdown report',
185+
processor: [self::class, 'process'],
186+
structuredContent: false
187+
)]
188+
class Report
189+
{
190+
public function __construct(
191+
private string $title,
192+
private string $content,
193+
) {}
194+
195+
// getters and setters...
196+
197+
public static function process($data): CallToolResult
198+
{
199+
$markdown = "# {$data->getTitle()}\n\n{$data->getContent()}";
200+
201+
return new CallToolResult(
202+
[new TextContent($markdown)],
203+
false
204+
);
205+
}
206+
}
207+
```
208+
209+
Setting `structuredContent: false` disables the automatic JSON serialization. When returning a `CallToolResult`, the response is sent as-is to the AI agent.
210+
211+
## Validation
212+
213+
MCP tool inputs support validation using the same mechanisms as regular API Platform operations.
214+
215+
On Symfony, use [Symfony Validator constraints](../symfony/validation.md):
216+
217+
```php
218+
<?php
219+
namespace App\ApiResource;
220+
221+
use ApiPlatform\Metadata\McpTool;
222+
use Symfony\Component\Validator\Constraints as Assert;
223+
224+
#[McpTool(
225+
name: 'submit_contact',
226+
description: 'Submit a contact form',
227+
processor: [self::class, 'process']
228+
)]
229+
class ContactForm
230+
{
231+
#[Assert\NotBlank]
232+
#[Assert\Length(min: 3, max: 50)]
233+
private ?string $name = null;
234+
235+
#[Assert\NotNull]
236+
#[Assert\Email]
237+
private ?string $email = null;
238+
239+
#[Assert\Positive]
240+
private ?int $age = null;
241+
242+
// getters, setters, and processor...
243+
}
244+
```
245+
246+
On Laravel, use [validation rules](../laravel/validation.md):
247+
248+
```php
249+
#[McpTool(
250+
name: 'submit_contact',
251+
description: 'Submit a contact form',
252+
processor: [self::class, 'process'],
253+
rules: [
254+
'name' => 'required|min:3|max:50',
255+
'email' => 'required|email',
256+
]
257+
)]
258+
```
259+
260+
## Declaring MCP Resources
261+
262+
MCP resources expose read-only content that AI agents can retrieve — documentation, configuration, reference data, etc. Use the `McpResource` attribute with a [state provider](state-providers.md):
263+
264+
```php
265+
<?php
266+
namespace App\ApiResource;
267+
268+
use ApiPlatform\Metadata\ApiResource;
269+
use ApiPlatform\Metadata\McpResource;
270+
271+
#[ApiResource(
272+
operations: [],
273+
mcp: [
274+
'api_docs' => new McpResource(
275+
uri: 'resource://my-app/documentation',
276+
name: 'App-Documentation',
277+
description: 'Application API documentation',
278+
mimeType: 'text/markdown',
279+
provider: [self::class, 'provide']
280+
),
281+
]
282+
)]
283+
class Documentation
284+
{
285+
public function __construct(
286+
private string $content,
287+
private string $uri,
288+
) {}
289+
290+
// getters and setters...
291+
292+
public static function provide(): self
293+
{
294+
return new self(
295+
content: '# My API Documentation\n\nWelcome to the API.',
296+
uri: 'resource://my-app/documentation'
297+
);
298+
}
299+
}
300+
```
301+
302+
The `uri` must be unique across the MCP server and follows the `resource://` URI scheme.
303+
304+
## McpTool Options
305+
306+
The `McpTool` attribute accepts all standard [operation options](operations.md) plus:
307+
308+
| Option | Description |
309+
|---|---|
310+
| `name` | Tool name exposed to AI agents (defaults to the class short name) |
311+
| `description` | Human-readable description of the tool (defaults to class DocBlock) |
312+
| `structuredContent` | Whether to include JSON structured content in responses (default: `true`) |
313+
| `input` | A separate DTO class to use as the tool's input schema |
314+
| `output` | A separate DTO class to use as the tool's output representation |
315+
| `annotations` | MCP tool annotations describing behavior hints |
316+
| `icons` | List of icon URLs representing the tool |
317+
| `meta` | Arbitrary metadata |
318+
| `rules` | Laravel validation rules (Laravel only) |
319+
320+
## McpResource Options
321+
322+
The `McpResource` attribute accepts all standard [operation options](operations.md) plus:
323+
324+
| Option | Description |
325+
|---|---|
326+
| `uri` | Unique URI identifying this resource (required, uses `resource://` scheme) |
327+
| `name` | Human-readable name for the resource |
328+
| `description` | Description of the resource (defaults to class DocBlock) |
329+
| `structuredContent` | Whether to include JSON structured content (default: `true`) |
330+
| `mimeType` | MIME type of the resource content |
331+
| `size` | Size in bytes, if known |
332+
| `annotations` | MCP resource annotations |
333+
| `icons` | List of icon URLs |
334+
| `meta` | Arbitrary metadata |

outline.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ chapters:
5555
- dto
5656
- openapi
5757
- json-schema
58+
- mcp
5859
- mercure
5960
- push-relations
6061
- errors

0 commit comments

Comments
 (0)