Skip to main content
Version: 2026.2

Custom Field Hint Providers

When your bundle introduces custom data types (via ClassDefinition\Data subclasses), the agent won't know what data format to produce for those fields. Register a custom field hint provider so that get_class_definition and get_field_schema return correct format hints.

For the architectural picture - the provider pattern, the registry, the recursion depth guard, and the catalog of types the bundle already covers - see Architecture → Field Hint System. Check the catalog first: if your type is already covered by a built-in provider, you do not need a custom one.

When You Need This​

  • Your bundle adds a custom Pimcore data type (e.g. colorPicker, currencyField)
  • An existing type needs a more specific hint than the default <value> fallback
  • You want to include custom metadata (valid values, constraints) in the schema

Creating a Provider​

Implement FieldFormatHintProviderInterface:

<?php

declare(strict_types=1);

namespace Vendor\Bundle\MyBundle\Mcp\FieldHint;

use Pimcore\Bundle\PimcoreAgentBundle\Mcp\FieldHint\FieldFormatHint;
use Pimcore\Bundle\PimcoreAgentBundle\Mcp\FieldHint\FieldFormatHintProviderInterface;
use Pimcore\Bundle\PimcoreAgentBundle\Mcp\FieldHint\FieldFormatHintRegistry;
use Pimcore\Model\DataObject\ClassDefinition\Data;

final readonly class ColorPickerHintProvider implements FieldFormatHintProviderInterface
{
public function getSupportedFieldTypes(): array
{
return ['colorPicker'];
}

public function getFormatHint(
Data $fieldDef,
FieldFormatHintRegistry $registry
): FieldFormatHint {
return new FieldFormatHint(
editableDataFormat: '"#RRGGBB"',
example: '"#FF6600"',
metadata: ['format' => 'hex color with # prefix']
);
}
}

getSupportedFieldTypes()​

Return the Pimcore field type identifiers your provider handles. These must match the string returned by $fieldDef->getFieldType(). One provider can handle multiple types.

getFormatHint()​

Receives the field definition and the registry (for recursive resolution of child fields). Return a FieldFormatHint with:

PropertyRequiredPurpose
editableDataFormatYesFormat string the LLM uses to compose data
exampleNoConcrete sample value
childrenNoNested field schemas (for composite types)
metadataNoConstraints, options, valid values

Registering the Provider​

Tag the service with pimcore_agent.field_format_hint_provider:

# config/services.yaml
services:
Vendor\Bundle\MyBundle\Mcp\FieldHint\ColorPickerHintProvider:
tags: [{ name: pimcore_agent.field_format_hint_provider }]

The registry picks up all tagged providers at boot. If multiple providers claim the same field type, the first registered provider wins.

Composite Types with Children​

For types that contain nested fields (like blocks or field collections), use the registry to recursively resolve child hints:

public function getFormatHint(
Data $fieldDef,
FieldFormatHintRegistry $registry
): FieldFormatHint {
$children = [];
foreach ($fieldDef->getFieldDefinitions() as $child) {
$hint = $registry->getHint($child);
$children[] = [
'name' => $child->getName(),
'type' => $child->getFieldType(),
'editableDataFormat' => $hint->editableDataFormat,
'example' => $hint->example,
'metadata' => $hint->metadata,
];
}

return new FieldFormatHint(
editableDataFormat: '[{"<field>": <value>, ...}]',
children: $children
);
}

The registry enforces a maximum recursion depth of 4 levels. Beyond that, child fields receive the generic <value> fallback. You don't need to implement depth checking yourself.

Including Metadata​

Metadata helps the LLM make valid choices. Common patterns:

// Enum-like constraints
new FieldFormatHint(
editableDataFormat: '"<status>"',
metadata: ['options' => ['draft', 'review', 'published']]
);

// Numeric constraints
new FieldFormatHint(
editableDataFormat: '<number>',
metadata: ['min' => 0, 'max' => 100, 'decimalPrecision' => 2]
);

// Units
new FieldFormatHint(
editableDataFormat: '{"value": <number>, "unitId": "<unit>"}',
metadata: ['validUnits' => ['kg', 'lb', 'oz'], 'defaultUnit' => 'kg']
);

Overriding Built-in Providers​

The registry uses the first registered provider for each field type. Since the bundle's providers are registered before user bundle providers in the tagged iterator, overriding a built-in provider requires configuring your service with a higher priority via the priority tag attribute, or using a compiler pass to replace the built-in provider's service definition.

Testing​

Test your provider in isolation by constructing it directly and calling getFormatHint():

public function testColorPickerFormat(): void
{
$provider = new ColorPickerHintProvider();

$fieldDef = $this->createMock(Data::class);
$fieldDef->method('getFieldType')->willReturn('colorPicker');

$registry = $this->createMock(FieldFormatHintRegistry::class);

$hint = $provider->getFormatHint($fieldDef, $registry);

self::assertSame('"#RRGGBB"', $hint->editableDataFormat);
self::assertSame('"#FF6600"', $hint->example);
self::assertArrayHasKey('format', $hint->metadata);
}

For integration testing, verify that get_class_definition returns your format hint for a class that uses your custom field type.