api-platform-mcp skill
Exposes API Platform resources as Model Context Protocol tools and resources for AI agents using PHP attributes.
- GitHub stars
- 29
- Files
- 1
- Last commit
- Oct 22, 2026
- License
- —
- Allowed tools
- —
What does the api-platform-mcp skill do?
api-platform-mcp exposes API Platform resources as Model Context Protocol tools and resources for AI agents using PHP attributes.
Summary generated from the skill's SKILL.md.
About the api-platform-mcp skill
Exposing PHP-based API resources as Model Context Protocol tools and resources is what this skill enables for developers using API Platform. It allows AI agents to discover and interact with your existing backend logic by transforming PHP classes into callable tools or read-only resources. This is achieved through specialized attributes like #[McpTool] for command execution and #[McpResource] for retrieving content like documentation or configuration.
Developers can integrate this into Symfony or Laravel applications to provide structured data or custom responses. The system reuses your existing metadata stack, including state processors, providers, and validation. You can define specific input schemas using DTOs, handle collections with McpToolCollection, or bypass standard JSON serialization by returning a CallToolResult. This ensures that an LLM can interact with your API through standard tools/call and resources/read operations without needing traditional HTTP routes.
What an agent can do with it
- 1Expose backend logic as toolsTurn a specific PHP class into a callable tool that an AI agent can use to process data.
- 2Enforce business rules on agent inputsEnsure that data sent by an AI agent adheres to specific validation constraints like email formats or string lengths.
Written from the skill's SKILL.md. Every use case is checked against a quote from it.
How to install the api-platform-mcp skill
Each command copies the skill folder into the client's skills directory. Restart the client or start a new session to load it.Install the api-platform-mcp skill in Plugin
/plugin marketplace add api-platform/skillset /plugin install api-platform@api-platform-skillset
Install the api-platform-mcp skill in Claude Code
~/.claude/skills/api-platform-mcp/SKILL.md
$ npx degit api-platform/skillset/skills/api-platform-mcp ~/.claude/skills/api-platform-mcpInstall the api-platform-mcp skill in Claude Code (project)
.claude/skills/api-platform-mcp/SKILL.md
$ npx degit api-platform/skillset/skills/api-platform-mcp .claude/skills/api-platform-mcpInstall the api-platform-mcp skill in Codex
~/.agents/skills/api-platform-mcp/SKILL.md
$ npx degit api-platform/skillset/skills/api-platform-mcp ~/.agents/skills/api-platform-mcpInstall the api-platform-mcp skill in Gemini CLI
~/.gemini/skills/api-platform-mcp/SKILL.md
$ npx degit api-platform/skillset/skills/api-platform-mcp ~/.gemini/skills/api-platform-mcpInstall the api-platform-mcp skill in Claude.ai
npx degit api-platform/skillset/skills/api-platform-mcp api-platform-mcp && zip -r api-platform-mcp.zip api-platform-mcp # claude.ai → Customize → Skills → + → Create skill → Upload a skill → api-platform-mcp.zip
SKILL.md
View on GitHubMCP: Exposing Your API to AI Agents
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.
Install & enable
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.
Two ways to declare a tool
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;
}
Separate input DTO
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).
Collections — McpToolCollection
Use 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.
Custom responses — CallToolResult
Default: 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.
Schema overrides — the nullable-union gotcha
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"]}
}
Validation — off by default
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'])]
Read-only resources — #[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');
}
}
Options
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 |
Wire protocol (for testing)
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.
Laravel
- Config under
mcpinconfig/api-platform.php; endpoint auto-registered at/mcp. - Validation uses
rules:(string/array/closure/FormRequest), not Symfony constraints. - MongoDB is not supported for MCP.
Checklist
-
api-platform/mcp+symfony/mcp-bundleinstalled,/mcptransport configured (Symfony) - Tool class has a
processor(write) orMcpResourcehas aprovider(read) -
operations: []when the class is MCP-only (no HTTP routes) -
name:omitted insidemcp:array entries (the array key is the name) -
McpToolCollection(notMcpTool) for list-returning tools -
structuredContent: false+CallToolResultwhen bypassing JSON serialization - Nullable/union properties overridden with
#[ApiProperty(schema: …)]if the LLM rejects them -
validate: trueset explicitly where business constraints must run -
McpResourceuriis unique and usesresource://
Files in this skill
1api-platform-mcp skill questions
How do I install the api-platform-mcp skill in Claude Code?
Run npx degit api-platform/skillset/skills/api-platform-mcp ~/.claude/skills/api-platform-mcp. Claude Code loads skills from ~/.claude/skills (all projects) or .claude/skills (one project) and uses a skill when its description matches the task.
Which clients can use the api-platform-mcp skill?
Any client that reads the Agent Skills format: Claude Code, Codex, Gemini CLI and claude.ai (upload as a ZIP). Scripts in the skill run only where the client allows code execution.
How can I return a list of items to an AI agent?
Use the McpToolCollection class instead of McpTool when a tool needs to return a collection of items.
Does API Platform run its standard validation for MCP tools?
No, API Platform's validation pipeline is disabled by default for MCP tools; you must explicitly set validate: true to run business constraints.
How do I handle nullable properties that LLMs might reject?
You can override the JSON-Schema for specific properties using the #[ApiProperty(schema: [...])] attribute.