Custom MCP Tools
Any Symfony bundle can contribute MCP tools to the agent system. Tools are registered via the pimcore.mcp_tool
service tag and automatically become available through direct MCP servers and the meta-tool.
Creating a Tool
A tool is a PHP class with one or more methods annotated with #[McpTool]:
<?php
declare(strict_types=1);
namespace Vendor\Bundle\MyBundle\Mcp\Tool;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;
use Mcp\Schema\Content\TextContent;
use Mcp\Schema\Result\CallToolResult;
use Psr\Log\LoggerInterface;
use Throwable;
final readonly class SearchProductsTool
{
public function __construct(
private ProductServiceInterface $productService,
private LoggerInterface $logger
) {
}
#[McpTool(
name: 'my_search_products',
description: 'Search for products by category and price range. '
. 'Returns product names, prices, and IDs.'
)]
public function execute(
#[Schema(
type: 'string',
description: 'Product category (e.g. "Electronics", "Clothing").'
)]
string $category,
#[Schema(
type: 'integer',
description: 'Maximum price in cents. Omit for no limit.',
minimum: 0
)]
?int $maxPrice = null,
#[Schema(
type: 'integer',
description: 'Results per page. Default: 20, max: 50.',
minimum: 1,
maximum: 50
)]
int $pageSize = 20
): CallToolResult {
try {
$results = $this->productService->search($category, $maxPrice, $pageSize);
return new CallToolResult(
[new TextContent(json_encode($results, JSON_PRETTY_PRINT))],
isError: false
);
} catch (Throwable $e) {
$this->logger->error('Product search failed', ['exception' => $e]);
return new CallToolResult(
[new TextContent(json_encode(['error' => $e->getMessage()]))],
isError: true
);
}
}
}
Multi-Tool Classes
A single class can expose multiple tools by annotating multiple methods with #[McpTool]:
final readonly class ProductCrudTool
{
public function __construct(
private ProductServiceInterface $productService,
private LoggerInterface $logger
) {
}
#[McpTool(
name: 'get_product',
description: 'Get a product by ID.'
)]
public function get(
#[Schema(type: 'integer', description: 'Product ID.')]
int $id
): CallToolResult {
// ...
}
#[McpTool(
name: 'update_product',
description: 'Update a product by ID.'
)]
public function update(
#[Schema(type: 'integer', description: 'Product ID.')]
int $id,
#[Schema(type: 'string', description: 'JSON object of fields to update.')]
string $fields
): CallToolResult {
// ...
}
}
Each #[McpTool]-annotated method is registered as a separate tool. Tool names must be unique within a group.
Key Conventions
- Tool naming: Keep names short and descriptive. The MCP server provides the namespace, so prefixes are unnecessary. Avoid prefixes - the Copilot SDK adds its own server-key prefix, and the combined name must stay under 64 characters
- Descriptions: Write for LLMs, not API docs. Include examples and reference other tools
- Response format: Return JSON via
TextContent. Keep responses compact - LLMs process text, not UI payloads - Error handling: Catch exceptions, log them, and return
isError: truewith a message
Registering the Tool
Tag the service with pimcore.mcp_tool and a group attribute:
# In your bundle's services.yaml
services:
Vendor\Bundle\MyBundle\Mcp\Tool\SearchProductsTool:
tags:
- { name: 'pimcore.mcp_tool', group: 'my-products' }
This automatically:
- Creates a direct MCP server endpoint at
/pimcore-mcp/agent/my-products - Makes the tool discoverable via the meta-tool server
- Includes it in the
McpToolRegistryfor programmatic access
How Registration Works
The McpToolRegistryPass compiler pass runs at container compilation time. It collects all
services tagged with pimcore.mcp_tool, reads their #[McpTool] attributes via reflection,
groups them by the tag's group attribute, and creates per-group ServiceLocator instances.
The McpServerFactory then creates MCP server endpoints for each group on demand.
Tool names must be unique within a group - the compiler pass throws an error on duplicates.
Adding to an Existing Group
To extend a built-in group (e.g. adding a custom data object tool):
services:
Vendor\Bundle\MyBundle\Mcp\Tool\MyCustomObjectTool:
tags:
- { name: 'pimcore.mcp_tool', group: 'pimcore-data-objects-read' }
The tool is added to the existing pimcore-data-objects-read server alongside the built-in tools.
Group Descriptions
The first tool tag in a group with a description attribute sets the group description (shown in meta-tool discovery):
services:
Vendor\Bundle\MyBundle\Mcp\Tool\ListProductsTool:
tags:
- name: 'pimcore.mcp_tool'
group: 'my-products'
description: 'Search and manage products'
Using the Tool in Agents
Add the group to an agent's configuration:
# agent-server/config/agents/product-agent.yaml
name: product-agent
displayName: Product Agent
description: Search and manage products
pimcoreMcpServers:
- my-products # Direct access
# Or as meta-tool (on-demand discovery):
pimcoreMetaGroups:
- my-products