Skip to main content
Version: 2026.1

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.