aashan / pimcore-mcp-bundle
Pimcore MCP bundle for agentic coding / orchestration and information retrival
Package info
github.com/aashan10/pimcore-mcp-bundle
Type:symfony-bundle
pkg:composer/aashan/pimcore-mcp-bundle
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.0
- league/html-to-markdown: ^5.0
- mcp/sdk: ^0.7
- pimcore/pimcore: ^11.0
- symfony/config: ^6.4 || ^7.0
- symfony/console: ^6.4 || ^7.0
- symfony/css-selector: ^6.4 || ^7.0
- symfony/dependency-injection: ^6.4 || ^7.0
- symfony/dom-crawler: ^6.4 || ^7.0
- symfony/http-kernel: ^6.4 || ^7.0
README
An MCP server for Pimcore that exposes the DataObject schema (classes, field collections, object bricks) to coding agents. It is built for assisting development: every tool returns the essential information only, and offers an explicit way to expand any single field into its complete configuration on demand.
Schema is read directly from the generated definition files, so no database connection is required.
Why not symfony/mcp-bundle?
symfony/mcp-bundle requires Symfony 7, but Pimcore 11 pins Symfony 6.4. This
bundle therefore depends on the underlying SDK, mcp/sdk
(^0.7, compatible with Symfony ^6.4), directly and provides its own thin
wiring: a console command that runs the stdio server plus service registration.
Installation
composer require aashan/pimcore-mcp-bundle
Register the bundle in config/bundles.php:
Aashan\PimcoreMcpBundle\PimcoreMcpBundle::class => ['all' => true],
Running
The server speaks JSON-RPC 2.0 over stdio:
bin/console pimcore:mcp:serve
Point an MCP client at that command. Example (Claude Code / Claude Desktop), running inside the Pimcore container:
{
"mcpServers": {
"pimcore": {
"command": "docker",
"args": ["compose", "exec", "-T", "php", "php", "bin/console", "pimcore:mcp:serve"]
}
}
}
stdout is the JSON-RPC channel — the command writes nothing else to it.
Tools
| Tool | Purpose |
|---|---|
list_data_object_classes |
Index of all DataObject classes (name, title, group, description). |
get_data_object_class |
Essential field & layout tree of one class. |
describe_field |
Complete, verbatim configuration of one class field. |
list_field_collections |
Index of all field collections. |
get_field_collection |
Essential field & layout tree of one field collection. |
list_object_bricks |
Index of all object bricks, incl. the classes/fields they attach to. |
get_object_brick |
Essential field & layout tree of one object brick. |
describe_container_field |
Complete configuration of one field on a collection/brick. |
get_data_object_methods |
Public PHP method signatures (typed getters/setters) of a generated DataObject class. |
get_object_brick_methods |
Public PHP method signatures of a generated object brick data class. |
get_field_collection_methods |
Public PHP method signatures of a generated field collection data class. |
list_website_settings |
List website settings (filter by name, site id, language). |
get_website_setting |
Get one website setting by id, or by name (+ site/language). |
create_website_setting |
Create a website setting. |
update_website_setting |
Update a website setting by id (only provided fields change). |
delete_website_setting |
Delete a website setting by id. |
create_definition |
Create a class / field collection / object brick (optionally with fields). |
add_field |
Add a field to a class / field collection / object brick definition. |
update_field |
Update an existing field's configuration. |
remove_field |
Remove a field (drops its DB column). |
export_class / import_class |
Export a class definition as JSON / create-or-overwrite from JSON. |
list_children |
List the children of a document/asset/data-object tree node. |
get_element |
Inspect one document/asset/data object (metadata + content). |
list_document_types / create_document_type / update_document_type / delete_document_type |
Manage predefined document types. |
list_predefined_properties / create_predefined_property / update_predefined_property / delete_predefined_property |
Manage predefined properties. |
get_frontend_errors |
Read browser-captured JS errors / rejections / console.error+warn from the running site. |
frontend_error_stats |
Counts of stored browser errors/warnings by type and surface. |
clear_frontend_errors |
Delete all stored browser errors/warnings (reset before reproducing). |
list_log_files |
List the Monolog log files in the log directory. |
list_log_channels |
List the Monolog channels present in the log files. |
get_log_entries |
Return the last N log entries, filtered by channel / minimum level. |
system_info |
Pimcore/PHP versions, environment, languages, sites, bundles, maintenance mode. |
list_container_services |
List Symfony container services, filtered by tag, class (substring / namespace prefix), id and visibility. |
list_service_tags |
Index of every service tag with an occurrence count each. |
describe_service |
Full definition of one service/alias: class, flags, factory, tags (with attributes), method calls, aliases. |
list_translations / set_translation / delete_translation |
Manage shared/admin translations. |
generate_areabrick |
Scaffold a document areabrick (PHP class + Twig template). |
find_objects |
Find data objects by class + field conditions (with previews, sorting, paging). |
find_elements |
Find documents/assets by path, type and name. |
run_maintenance |
Run a safe allowlisted maintenance command (cache clear, classes rebuild, reindex). |
list_documentation_topics |
Browse the official docs.pimcore.com navigation for the running product version. |
get_documentation_page |
Fetch one documentation page as markdown. |
Essential vs. expanded
get_* tools return a compact tree. Each node carries:
kind—data(a value field / getter-setter) orlayout(a structural container);fieldtype— the Pimcore type (input,manyToOneRelation,localizedfields,panel, …);title;attributes— only the highlights that affect usage:mandatory,allowedClasses(relations),options/optionsProvider(selects),allowedTypes(bricks/collections),maxLength,unique,readOnly,invisible;children— nested nodes (layout children, or the sub-fields oflocalizedfields/block).
Every get_* schema response includes a _note telling the agent to call
describe_field / describe_container_field with a field name to retrieve the
field's full configuration (validators, defaults, column types, widths,
tooltips, permissions, …).
PHP API (method signatures)
The *_methods tools reflect the generated model classes
(Pimcore\Model\DataObject\<Class>, …\Objectbrick\Data\<Key>,
…\Fieldcollection\Data\<Key>) and return their public method signatures — the
typed getters/setters an agent needs to write correct code, e.g.
setName(?string $name, ?string $language = null): static or
getPower(): ?\Pimcore\Model\DataObject\Data\QuantityValue. Each accessor is
tagged with the field it maps to.
Only schema-specific methods are listed; standard model methods (save,
getById, getList, …) are inherited from the class reported in extends.
These tools read compiled PHP classes, so they require the class files to exist
(bin/console pimcore:deployment:classes-rebuild regenerates them).
Website settings (CRUD)
The *_website_setting(s) tools read and write the website_settings table
via Pimcore's WebsiteSetting model — these are live database
operations and require a configured DB connection.
type is one of text, document, asset, object, bool. data is
exchanged as a string and coerced to the type: bool accepts
true/false/1/0; document/asset/object take the referenced element id
(element-typed values are returned as a {elementType, id, path} reference).
update_website_setting only changes the fields you pass; delete_website_setting
permanently removes the row.
Adding fields (schema mutation)
add_field appends a field to a class, field collection or object brick
definition and persists it using Pimcore's own layout-tree builder
(ClassDefinition\Service::generateLayoutTreeFromArray). Saving regenerates
the PHP model classes and alters the database tables, so it requires a DB and
a writable class-definition directory.
Arguments: scope (class | fieldcollection | objectbrick), container
(class name or collection/brick key), name, fieldtype (input, numeric,
select, manyToOneRelation, …), optional title, mandatory, parent
(name of a layout element / container field such as localizedfields to nest
under — defaults to the root), and config — a JSON object of field-type
specific settings merged into the definition, e.g.:
- input:
{"maxLength": 50} - select:
{"options": [{"key": "A", "value": "a"}]} - manyToOneRelation:
{"classes": [{"classes": "Manufacturer"}]}
The tool returns the updated essential schema. Note: the new typed
getter/setter reported by the *_methods tools only appears on the next server
start, because the PHP class is already loaded in the running process.
Re-saving a definition rewrites its definition file in the current Pimcore export format; a file generated by an older version will show reordered properties and normalised defaults. This is cosmetic — the schema is unchanged.
The full authoring set:
create_definition(scope, name, fields?, options?)— create aclass,fieldcollectionorobjectbrick.fieldsis a JSON array of field specs (same shape asadd_field);optionsis a JSON object (class:description/group/parentClass/allowInherit/allowVariants; collection/brick:title/group; brick alsoclassDefinitions).update_field(scope, container, field, config)— change a field's config (title, mandatory, validators, options, …); name/type are immutable.remove_field(scope, container, field)— remove a field; drops its DB column.export_class(className)→ JSON, andimport_class(className, json, overwrite?)— clone a class or restore it from an exported definition (Pimcore's native JSON), useful for scaffolding from a spec or moving a class between projects.
After these, run run_maintenance (rebuild_classes / clear_cache) so the
generated PHP model classes and DB tables are fully in sync.
Inspecting content (documents, assets, data objects)
list_children and get_element browse and inspect the actual content trees.
Both take an elementType of document, asset or object and address a
node by id or path (list_children defaults to the root /).
list_childrenreturns lightweight summaries (id,type,key,path,published,hasChildren) plustotal, for top-down tree navigation and pagination (limit/offset,includeUnpublished).get_elementreturns full detail: common metadata + properties, and type-specific content — data object field values (including object bricks, field collections and per-language localized fields), document editables (controller/template/title), or asset metadata (mime type, size, image dimensions).
Output is bounded: referenced elements become {type, id, path} references
(never expanded inline), nested structures stop at a fixed depth, lists are
capped, and long strings (e.g. WYSIWYG) are truncated.
Predefined admin config (CRUD)
Full CRUD over Pimcore's predefined configuration (stored via the location-aware config / settings store):
- Document types —
list/create/update/delete_document_type. A document type presets thecontroller/template/groupfor a document kind (type:page,snippet,email,link, …). Ids are auto-generated UUIDs. - Predefined properties —
list/create/update/delete_predefined_property.typeis one oftext,document,asset,object,bool,select;ctype(the element it applies to) isdocument,assetorobject; plus optionaldata(default value),config,description,inheritable.
update_* changes only the fields you pass; create_*/update_* return the
full record.
Logs
list_log_files, list_log_channels and get_log_entries read the Monolog
log files in the configured log directory (%kernel.logs_dir%, e.g. var/log).
get_log_entries returns the last lines entries (default 100, max 1000) and
respects the Monolog channels written into each record
([time] channel.LEVEL: message):
channel— filter to one channel (e.g.console,request,admin_statistics);level— minimum severity (DEBUG…EMERGENCY), soERRORalso returnsCRITICAL+;file— restrict to one log file; omitted, it searches all files and merges by time;scanKb— how much of each file's tail to scan (default 2048).
Only the tail of each file is read, so this is safe on multi-hundred-MB logs.
Use list_log_channels to discover which channels actually appear in the logs.
Service container introspection
list_container_services, list_service_tags and describe_service expose the
Symfony service container so an agent can discover extension points (event
subscribers, Twig extensions, message handlers, …) and see how a service is
wired.
Service tags, and the classes of private services, are compile-time
metadata that the live runtime container has already stripped. These tools
therefore read the debug container XML dump — the same source
bin/console debug:container uses (%debug.container.dump%), loaded into a
standalone ContainerBuilder. No service is instantiated and no database is
touched. The dump only exists when the kernel runs in debug mode
(APP_ENV=dev / APP_DEBUG=1); otherwise the tools return an error saying so.
list_container_services(tag?, class?, idLike?, public?, limit?, offset?)— filter bytag(exact tag name),class(case-insensitive substring, so a namespace prefix likePimcore\Bundlescopes by vendor/bundle),idLike(substring on the service id) andpublic(true/falsevisibility). Filters combine (AND). Each entry is compact:id,class,publicand its tag names.list_service_tags()— every tag name in the container with a count, for discovering which tags to filter by (mirrorslist_log_channels).describe_service(id)— the full definition of one service or alias: class,public/shared/abstract/lazy/synthetic/deprecated/autowiredflags,factory,tags(with their attributes),methodCallsandaliases.
Frontend (browser) errors
get_frontend_errors, frontend_error_stats and clear_frontend_errors let an
agent see what went wrong in the browser — the half of a Pimcore project the
server-side logs never see. A tiny collector script, auto-injected into HTML
pages in debug mode, captures uncaught JS errors (window.onerror),
unhandled promise rejections and console.error / console.warn, batches them
and ships them (via sendBeacon, falling back to fetch keepalive) to an
ingest endpoint. The reports are stored as JSON Lines in the log directory
(frontend-errors.jsonl, rotated at 5 MB) — no database required.
The whole path is zero-config and debug-only:
- A high-priority
kernel.requestlistener owns the fixed path/_pimcore-mcp/client-errors, short-circuiting it before routing and the security firewall — so there is no route to register and no authentication to satisfy. Outside debug mode the path is not handled at all, so the collector effectively does not exist in production. - A
kernel.responselistener inserts the collector<script>as the first thing inside<head>for full HTML documents on the main request — covering both the public website and the Pimcore admin. It must run before any other script:window.onerror, theerrorevent and theconsole.error/warnwrappers only capture what happens after they are installed, so an inline classic script at the top of<head>(which executes before module/deferred scripts) is what makes the page's own head/body scripts observable. JSON/XHR responses, redirects, downloads, streamed responses and the ingest endpoint itself are left untouched, and double injection is guarded.
Each stored report carries type (error / unhandledrejection /
console.error / console.warn), message, stack, source, line/col,
the page url, a derived surface (website or admin), the server-observed
ua/ip and both client and server timestamps. Free-text fields are clipped
and each beacon is capped (≤ 64 KB, ≤ 50 reports) so the store cannot be
flooded.
get_frontend_errors(limit?, type?, surface?, sinceMs?)— recent reports (newest last), filterable by kind, by surface and by server timestamp.frontend_error_stats()— total plus counts grouped by type and surface.clear_frontend_errors()— reset the store; call it before reproducing an issue so only the fresh reports come back.
Typical loop: clear_frontend_errors → reproduce in the browser →
get_frontend_errors to read the stack traces the change produced.
System info & translations
system_inforeturns Pimcore & PHP versions, environment/debug, valid languages, configured sites, enabled bundles and maintenance mode — a quick way for an agent to orient itself.list_translations/set_translation/delete_translationmanage translations in themessages(shared) oradmindomain.set_translationis an upsert: passtranslationsas a JSON object oflanguage => text; provided languages merge into any existing entry.
Scaffolding areabricks
generate_areabrick creates a document areabrick following this project's
convention: the PHP class in src/Document/Areabrick/<Name>.php (extending the
project's AbstractAreabrick when present, else the framework
AbstractTemplateAreabrick) and its Twig view in
templates/areas/<brick-id>/view.html.twig. The brick id is the kebab-case of
the class name (matching Pimcore's AreabrickPass), so the template is
auto-discovered, and the brick auto-registers via container autoconfiguration.
Arguments: name (PascalCase), optional description, optional editables
(JSON array of {type, name, label?} to prefill the template with
pimcore_<type>('<name>') tags), and force to overwrite. Run
run_maintenance with clear_cache to see a new brick in the admin.
Finding content
find_objectslocates data objects of a class by validated field conditions.filtersis a JSON array of{field, operator, value}; operators are=, !=, >, >=, <, <=, LIKE, NOT LIKE, IN, NOT IN, IS NULL, IS NOT NULL; fields must be class fields or system fields (id,key,published,path, …), so conditions are built only from known identifiers with bound parameters (no arbitrary SQL). SupportspathPrefix,orderBy/orderDir, paging, and an optionalfieldslist to include value previews per result.find_elementslocates documents/assets bypathPrefix, nodetypeandnameLike. Both return lightweight summaries; useget_elementfor detail.
Maintenance (closing the loop)
run_maintenance runs a fixed allowlist of Pimcore commands as isolated
subprocesses — no user-supplied command ever reaches the shell:
| action | command |
|---|---|
clear_cache |
pimcore:cache:clear |
warmup_cache |
cache:warmup |
rebuild_classes |
pimcore:deployment:classes-rebuild |
reindex_search |
pimcore:search-backend-reindex |
Use it after add_field, generate_areabrick or content changes so they take
effect. Returns success, exit code and (tail-truncated) output.
Official documentation
list_documentation_topics and get_documentation_page serve the official
Pimcore documentation from docs.pimcore.com,
matched to the running product version — so an agent reads the docs that
correspond to the installed Pimcore, not a random version.
The docs are published per platform version under /platform/<version>
(e.g. /platform/2024.4). The version is resolved automatically:
- an explicit
docsVersionargument (YYYY.N), if given; - otherwise
Pimcore\Version::getPlatformVersion()(thepimcore/platform-versionpackage), reduced toYYYY.N; - otherwise, for Pimcore ≥ 11, the newest published version (never older than
2023.3, the first platform-versioned docs for Pimcore 11).
Pimcore < 11 is reported as unsupported. Every response reports the resolved
docsVersion, how it was chosen (versionSource), and the availableVersions
so the agent can pin a different one.
list_documentation_topics(section?, docsVersion?)reads the sidebar navigation (the page's<aside>) and returns the topic tree (title+ version-relativepath). The docs site only expands the subtree along the requested path, so passsection(e.g.PimcoreorPimcore/Objects) to drill in and reveal a topic's child pages.get_documentation_page(path, docsVersion?)fetches one page (by thepathfrom the navigation, e.g.Pimcore/Objects/Object_Classes) and returns its body converted to markdown. Output is capped;truncatedflags when the page was clipped andurllinks to the full page.
These tools make outbound HTTP to docs.pimcore.com (no DB required).
Architecture
src/
PimcoreMcpBundle.php Bundle entry point
DependencyInjection/ Loads config/services.yaml
Command/McpServerCommand.php `pimcore:mcp:serve` — builds & runs the stdio server
Tool/ MCP tools (#[McpTool] methods)
Documentation/DocumentationFetcher Fetches & parses docs.pimcore.com (version-aware)
Container/ServiceCatalog DB-free introspection of the debug service-container dump
Frontend/ Browser-error collector script + JSONL store
EventListener/ Ingest (kernel.request) + script injection (kernel.response)
Repository/ DB-free access to Pimcore definitions
Extractor/DefinitionExtractor Pimcore definitions -> compact DTOs
Entity/Definitions/ The DTOs (ClassDefinition, ContainerDefinition, FieldDefinition)
Tools are discovered from their #[McpTool] attributes and resolved through a
PSR-11 service locator, so they are ordinary autowired Symfony services.