From api-platform
Exposes API Platform resources as MCP tools and resources for AI agents. Use when building LLM-accessible APIs via Model Context Protocol.
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-platform:api-platform-mcpThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
API Platform turns PHP classes into [Model Context Protocol](https://modelcontextprotocol.io/)
API Platform turns PHP classes into Model Context Protocol tools and resources. It reuses the existing metadata stack — state processors/providers, validation, serialization, JSON Schema — so an MCP tool is just an operation with no HTTP route.
@experimental. API may change between minor releases.
Symfony — composer require api-platform/mcp symfony/mcp-bundle (bundle namespace is
Symfony\AI\McpBundle, composer name symfony/mcp-bundle):
# config/packages/mcp.yaml
mcp:
client_transports:
http: true
stdio: false
http:
path: "/mcp"
session: { store: "file", directory: "%kernel.cache_dir%/mcp", ttl: 3600 }
# config/packages/api_platform.yaml
api_platform:
mcp:
enabled: true # default: true
format: jsonld # default; must be a registered api_platform.format (json, jsonapi, …)
format sets the serialization format for structured content. jsonld emits @context,
@id, @type. Laravel — enabled by default, endpoint auto-registered at /mcp:
// config/api-platform.php
return [ /* … */ 'mcp' => ['enabled' => true] ];
API Platform registers its own SDK Handler; supports() returns true only for tools/resources
in its registry, otherwise the SDK's own handler chain runs. API Platform tools and native
mcp/sdk tools coexist on the same server.
Standalone #[McpTool] class attribute — convenient for a one-off tool. Class properties
become the inputSchema; a state processor handles the command
(CQRS-style — the agent's input is processed through your logic):
use ApiPlatform\Metadata\McpTool;
#[McpTool(
name: 'process_message',
description: 'Process a message with priority',
processor: [self::class, 'process'],
)]
class ProcessMessage
{
public function __construct(
private string $message,
private int $priority = 1,
) {}
// getters + setters drive (de)serialization
public static function process($data): mixed
{
$data->setMessage('Processed: '.$data->getMessage());
return $data;
}
}
The agent's arguments are deserialized into a ProcessMessage, passed to the processor; the
returned object is serialized back as structured content. processor is any callable or a
ProcessorInterface service.
mcp: array on #[ApiResource] — use when the class exposes no HTTP endpoints
(operations: []), combines several MCP operations, or needs resource-level control. Keys are
the tool names, so omit name: inside each new McpTool(...):
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\McpTool;
use App\State\ReadHydraResourceProcessor;
#[ApiResource(
operations: [],
mcp: [
'read_hydra_resource' => new McpTool(
description: 'Navigate to a Hydra API resource by URI.',
processor: ReadHydraResourceProcessor::class,
structuredContent: false,
),
],
)]
class ReadHydraResource
{
public string $uri;
}
When the input schema must differ from the result class, point input: at a DTO. That DTO drives
inputSchema; the host class describes the output:
#[McpTool(
name: 'search_books',
description: 'Search books by keyword',
input: SearchQuery::class, // what the agent sends
processor: SearchBooksProcessor::class,
)]
class BookSearchResult
{
public int $id;
public string $title;
public string $isbn;
}
The processor receives a SearchQuery instance and returns the result(s).
McpToolCollectionUse McpToolCollection (extends McpTool, implements CollectionOperationInterface) when a tool
returns a list — the serializer and schema factory emit collection-shaped output:
use ApiPlatform\Metadata\McpToolCollection;
#[ApiResource(operations: [], mcp: [
'list_books' => new McpToolCollection(
description: 'List Books',
input: SearchQuery::class,
processor: SearchBooksProcessor::class,
structuredContent: true,
),
])]
class Book { public ?int $id = null; public ?string $title = null; public ?string $isbn = null; }
With structuredContent: true and the default jsonld format, the response carries @context,
hydra:totalItems, and a hydra:member array of serialized items; the published outputSchema
reflects that shape. Output schemas are flattened for MCP compliance — no $ref, allOf,
$schema, or definitions.
CallToolResultDefault: results serialize through API Platform's serialization
as structured JSON. For full control, set structuredContent: false and return a CallToolResult:
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Result\CallToolResult;
#[McpTool(name: 'generate_report', description: 'Generate a markdown report',
processor: [self::class, 'process'], structuredContent: false)]
class Report
{
public function __construct(private string $title, private string $content) {}
// getters/setters …
public static function process($data): CallToolResult
{
return new CallToolResult(
[new TextContent("# {$data->getTitle()}\n\n{$data->getContent()}")],
false, // isError
// 3rd arg: optional _meta array
);
}
}
structuredContent: false disables automatic JSON serialization; the CallToolResult is sent
as-is, with no structuredContent / _meta key in the response.
PHP nullable types generate union schemas like {"type": ["array", "null"]}. Some LLM providers
reject these. Override per property with #[ApiProperty(schema: [...])]:
use ApiPlatform\Metadata\ApiProperty;
class InvokeHydraOperation
{
public string $uri;
public string $method;
#[ApiProperty(schema: ['type' => 'object', 'description' => 'JSON payload for the request'])]
public ?array $payload = null; // without override: {"type": ["array","null"]}
}
The MCP SDK validates inputs against the JSON Schema at transport level (types, required fields).
API Platform's own validation pipeline is disabled for MCP tools. Set validate: true to
run business constraints (email format, length, custom rules):
use Symfony\Component\Validator\Constraints as Assert;
#[McpTool(name: 'submit_contact', description: 'Submit a contact form',
processor: [self::class, 'process'], validate: true)]
class ContactForm
{
#[Assert\NotBlank] #[Assert\Length(min: 3, max: 50)]
private ?string $name = null;
#[Assert\NotNull] #[Assert\Email]
private ?string $email = null;
}
Laravel — same validate: true, plus rules: instead of constraints:
#[McpTool(name: 'submit_contact', description: '…', processor: [self::class, 'process'],
validate: true, rules: ['name' => 'required|min:3|max:50', 'email' => 'required|email'])]
#[McpResource]Expose retrievable content (docs, config, reference data) backed by a
state provider. uri must be unique and use the resource:// scheme:
use ApiPlatform\Metadata\McpResource;
#[ApiResource(operations: [], mcp: [
'api_docs' => new McpResource(
uri: 'resource://my-app/documentation',
name: 'App-Documentation',
description: 'Application API documentation',
mimeType: 'text/markdown',
provider: [self::class, 'provide'],
),
])]
class Documentation
{
public function __construct(private string $content, private string $uri) {}
public static function provide(): self
{
return new self('# My API Documentation\n\nWelcome.', 'resource://my-app/documentation');
}
}
McpTool and McpResource extend HttpOperation, so they accept all standard
operation options. MCP-specific / notable ones:
| Option | Tool | Res | Notes |
|---|---|---|---|
name | ✓ | ✓ | exposed to agents; defaults to class short name. Omit in mcp: array (key wins) |
description | ✓ | ✓ | defaults to class DocBlock |
structuredContent | ✓ | ✓ | include JSON structured content (default true) |
input / output | ✓ | separate DTO for input schema / output representation | |
inputFormats / outputFormats | ✓ | (de)serialization formats, e.g. ['json'], ['jsonld'] | |
contentNegotiation | ✓ | ✓ | default false for MCP |
validate | ✓ | run validation pipeline (default false for MCP) | |
rules | ✓ | Laravel validation rules (Laravel only) | |
annotations/icons/meta | ✓ | ✓ | behavior hints / icon URLs / arbitrary metadata |
uri | ✓ | required, resource:// scheme, unique across server | |
mimeType / size | ✓ | content MIME type / byte size if known |
Everything is JSON-RPC 2.0 POSTed to /mcp with
Accept: application/json, text/event-stream. initialize returns an mcp-session-id header;
send it back on every subsequent call. tools/list / resources/list enumerate; tools/call
(params: {name, arguments}) and resources/read (params: {uri}) invoke. The handler overrides
Accept to the operation's output format, so a jsonld tool always responds JSON-LD regardless
of use_symfony_listeners.
mcp in config/api-platform.php; endpoint auto-registered at /mcp.rules: (string/array/closure/FormRequest), not Symfony constraints.api-platform/mcp + symfony/mcp-bundle installed, /mcp transport configured (Symfony)processor (write) or McpResource has a provider (read)operations: [] when the class is MCP-only (no HTTP routes)name: omitted inside mcp: array entries (the array key is the name)McpToolCollection (not McpTool) for list-returning toolsstructuredContent: false + CallToolResult when bypassing JSON serialization#[ApiProperty(schema: …)] if the LLM rejects themvalidate: true set explicitly where business constraints must runMcpResource uri is unique and uses resource://npx claudepluginhub api-platform/skillset --plugin api-platformDesigns and implements Model Context Protocol servers with resources, tools, prompts, and security best practices. Use when architecting an MCP server or integrating a service as an MCP endpoint.
Builds, debugs, and extends MCP servers and clients. Scaffolds projects, implements tool handlers and resource providers, configures stdio/HTTP/SSE transport, validates schemas with Zod or Pydantic, and tests protocol compliance.
Builds, debugs, and extends MCP servers/clients using TypeScript or Python SDKs. Scaffolds projects, registers tools/resources, configures transports, validates schemas with Zod/Pydantic, and tests protocol compliance.