Skip to main content
Version: 2026.1

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: true with 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:

  1. Creates a direct MCP server endpoint at /pimcore-mcp/agent/my-products
  2. Makes the tool discoverable via the meta-tool server
  3. Includes it in the McpToolRegistry for 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