Skip to main content
Version: Next

MCP Tools

Pimcore Copilot ships four MCP (Model Context Protocol) tool handlers for automation actions. An AI agent that can call them can list the configured automation actions, start one, follow the resulting job run, and cancel it, without going through Pimcore Studio.

The handlers are ordinary services carrying #[McpTool] and #[Schema] attributes. They are not bound to any particular MCP server: this bundle provides the tools, and integrating them into a server is a separate step.

Requirements

The handlers are registered only when mcp/sdk ^0.7 is installed, so an installation without an MCP server is unaffected. This feature also raises Copilot's minimum pimcore/studio-backend-bundle to ^2026.3, which is where the shared MCP tool boundary lives.

The tools

ToolHandlerPermission
list_automation_actionsMcp\Tool\ListAutomationActionsTool::execute()pcp_general_permission + per-action execute
run_automation_actionMcp\Tool\RunAutomationActionTool::execute()pcp_general_permission + per-action execute
get_automation_action_runMcp\Tool\GetAutomationActionRunTool::execute()pcp_job_run_permission
cancel_automation_action_runMcp\Tool\CancelAutomationActionRunTool::execute()pcp_job_run_permission

All four are in the Pimcore\Bundle\CopilotBundle\Mcp\Tool namespace. Each returns a single JSON text content block.

list_automation_actions

Lists the automation actions the current user is allowed to execute.

ParameterTypeDescription
filterstring, optionalCase-insensitive substring match against name, title and group. Omit it or pass null to list every action.
{
"actions": [
{
"name": "CarVariantGenerator",
"title": "Car Variant Generator",
"description": "This action will create a variant of a car object.",
"group": "catalog",
"stepCount": 1,
"stepTypes": ["generic_variant_generator"],
"manualRunnable": true,
"triggers": {"manual": true, "event": false, "scheduled": false}
}
],
"skipped": 0
}

manualRunnable reports whether the action is triggerable at all; the triggers flags report which trigger types are enabled on it. skipped counts configurations that could not be read, for example a step implementation belonging to an uninstalled bundle. A single unreadable configuration is skipped rather than failing the whole call, and the count tells the agent the list is incomplete.

The response carries run-relevant summaries only, never the raw step configuration.

run_automation_action

Starts an action by name and returns {"jobRunId": 42} immediately. The action itself runs asynchronously through the Generic Execution Engine, so follow it with get_automation_action_run.

ParameterTypeDescription
namestring, requiredThe action configuration name, as returned by list_automation_actions.
elementsarray, optionalElements the action runs against, for example [{"type": "object", "id": 83}]. type is one of object, asset, document; id is a positive integer. Omit it, or pass null or [], to run with no elements.
environmentVariablesobject, optionalFlat key/value overrides matching the action's configured environment variables. Omit it, or pass null or {}, to keep the configured defaults.

Malformed elements entries are rejected with an invalid_request naming the offending index, rather than being coerced. A coerced entry would be dropped further down and the run would start against an empty selection while still reporting a job run id.

get_automation_action_run

Reports one job run. Owner-scoped: a job run started by another user reports as not found.

ParameterTypeDescription
jobRunIdinteger, requiredThe id returned by run_automation_action.
logTailinteger, optionalNumber of most recent log lines to include. Defaults to 20, maximum 200. Pass 0 to omit the log.
{
"id": 42,
"jobName": "ImageClassification",
"state": "running",
"currentStep": 0,
"currentMessage": "Starting Image Classification for /Car Images/jaguar/auto-3095119.jpg",
"canCancel": true,
"startedAt": 1787134430,
"lastUpdated": 1787134431,
"logs": [{"message": "Job Run ImageClassification (42) started.", "createdAt": "2026-08-19T10:13:50+00:00"}]
}

logs holds the job run's log verbatim, oldest first, which is the same text the Pimcore Studio job overview shows. A failing step writes its error message there, so those lines reach whichever model provider the agent uses. Pass logTail: 0 for actions whose steps handle data that must not leave the installation.

Unlike the Pimcore Studio job overview, pcp_job_runs_see_all_permission is deliberately not honoured here: an agent only ever sees job runs owned by the user whose credentials it acts under.

cancel_automation_action_run

Cancels a job run and returns {"cancelled": true, "jobRunId": 42}. Only the run's owner may cancel it.

ParameterTypeDescription
jobRunIdinteger, requiredThe job run to cancel.

Cancellation is cooperative: it requests cancellation rather than stopping execution instantly. The Generic Execution Engine checks for the request between steps, so a step already running when the request arrives still finishes before the job run stops. Cancelling a run that has already finished reports a conflict.

Permissions

MCP tools bypass the Symfony #[IsGranted] and kernel.exception pipeline that Studio controllers rely on, so each handler checks permissions itself, against the user the MCP request authenticated as. The permission keys map to the checkboxes documented under Permissions.

Beyond the coarse pcp_general_permission, both list_automation_actions and run_automation_action honour the per-configuration execute permission, which is the user and role grants configured on each action. Administrators and users holding pcp_configuration_permission may execute any action. list_automation_actions omits any action the current user may not execute, and run_automation_action refuses to start one, mirroring the Copilot chat action list.

Error results

Every failure is returned as a tool result with isError: true and a JSON body of {"error": ..., "code": ...}, never as a transport-level error. The code is what an agent branches on:

codeMeaningExamples
permission_deniedThe caller may not do this, and no retry will change that.Missing pcp_general_permission or pcp_job_run_permission; no execute grant on the action; cancelling a job run owned by someone else.
not_foundThe name or id does not resolve. Re-read it and try again.Unknown action name; unknown or foreign job run id.
invalid_requestThe call was malformed, or is not valid in the current state.A malformed elements entry; a JSON list where environmentVariables expects an object; cancelling a job run that is no longer running.
internal_errorSomething failed inside Pimcore.Anything else.

An internal_error reads {"error": "Internal error while executing <tool> (ref: <id>). The cause was written to the Pimcore application log.", "code": "internal_error"}. The exception class, message and stack trace go to the Pimcore log under that same ref rather than to the client, so a client-visible reply can be tied to the logged exception without disclosing anything.

Tool annotations

Each handler declares a display title and MCP tool annotations. list_automation_actions and get_automation_action_run are marked read-only; run_automation_action and cancel_automation_action_run are marked as mutating, so an MCP client that auto-approves read-only tools still asks before starting or cancelling a run.

Integrating the tools into an MCP server

The tools have to be exposed by an MCP server before an agent can call them. There are two ways to do that.

Option 1: expose them from your own MCP server

The four handlers are autowired services, so any bundle building its own Mcp\Server can register them. Give the SDK a PSR-11 container that resolves the handler classes, then add each handler:

# config/services.yaml of your own bundle. A service locator keys each entry by its service id,
# which is the class name the SDK looks up when it resolves the handler.
services:
my_bundle.mcp.tool_locator: !service_locator
- '@Pimcore\Bundle\CopilotBundle\Mcp\Tool\ListAutomationActionsTool'
- '@Pimcore\Bundle\CopilotBundle\Mcp\Tool\RunAutomationActionTool'
- '@Pimcore\Bundle\CopilotBundle\Mcp\Tool\GetAutomationActionRunTool'
- '@Pimcore\Bundle\CopilotBundle\Mcp\Tool\CancelAutomationActionRunTool'
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Discovery\DocBlockParser;
use Mcp\Capability\Discovery\SchemaGenerator;
use Mcp\Server;
use Pimcore\Bundle\CopilotBundle\Mcp\Tool\ListAutomationActionsTool;
use Pimcore\Bundle\StudioBackendBundle\Mcp\Tool\ToolInputSchemaNormalizer;

$method = new ReflectionMethod(ListAutomationActionsTool::class, 'execute');
$attribute = $method->getAttributes(McpTool::class)[0]->newInstance();
$schemaGenerator = new SchemaGenerator(new DocBlockParser());

$server = Server::builder()
->setServerInfo('My MCP Server', '1.0.0')
->setContainer($toolLocator)
->addTool(
handler: [ListAutomationActionsTool::class, 'execute'],
name: $attribute->name,
title: $attribute->title,
description: $attribute->description,
annotations: $attribute->annotations,
inputSchema: ToolInputSchemaNormalizer::normalize($schemaGenerator->generate($method)),
)
->build();

Two details are easy to miss:

  • addTool() does not read the #[McpTool] attribute. Without an explicit name the tool is registered under the method name, so all four would be called execute. Read the attribute by reflection, as above, and the attribute stays the single source of truth.
  • Normalize the generated input schema. ToolInputSchemaNormalizer widens the parameter types the SDK infers so that a JSON object parameter accepts object, array and null. Without it, environmentVariables rejects both {} and null.

The handlers check permissions against the user the request authenticated as, so the server must run behind a firewall that establishes a Pimcore backend user. They also assume nothing about the transport.

Option 2: use the Pimcore Agent Bundle

If the Pimcore Agent Bundle is installed, no integration work is needed: this bundle registers the four handlers with that bundle's pimcore.mcp_tool extension point, which builds the server, applies the schema normalization, and serves each group under /pimcore-mcp/agent/<group>.

The handlers are grouped the way the Agent Bundle groups its own tools, so an agent can be given inspection without also being given unattended execution:

GroupTools
pimcore-copilot-automation-readlist_automation_actions, get_automation_action_run
pimcore-copilot-automation-direct-writerun_automation_action, cancel_automation_action_run

Attach them to an agent through its pimcoreMcpServers list (tool schemas sent with every turn) or pimcoreMetaGroups list (discovered on demand through the meta-tool). Agents are defined on the Pimcore side, either in a config/agents/<agent>.yaml preset, in the project's config/packages/*.yaml, or in Pimcore Studio under System -> Pimcore Agent -> Agent Configuration:

pimcoreMcpServers:
- pimcore-copilot-automation-read
- pimcore-copilot-automation-direct-write

To confirm the live registration:

bin/console pimcore-agent:mcp:list-tools

See Custom MCP Tools for the extension contract these groups follow.

Recursion warning

If a custom automation action step invokes an AI agent (for example, a step implementation that calls the Pimcore Agent Bundle to run an LLM turn as part of the job), and that agent can also call run_automation_action, the agent can start the action again, including the one that triggered it. This can loop indefinitely and exhaust the Generic Execution Engine's job queue.

Only give run_automation_action to agents that genuinely need to orchestrate automation actions, and keep it out of the tool set of any agent invoked from inside an automation action step.