Extending Bricklayer: adding your own MCP tool

Just enough code to add a new MCP tool to Bricklayer.

First, make sure You actually need a tool. Project knowledge (conventions, integrations, house rules) belongs in .bricklayer/ as skills and guidelines, no PHP required. A tool is for a new capability: something the agent should be able to ask the running Magento instance that the existing 84 tools can not answer.

Also check whether an existing consolidated tool can take it. system-status check=..., log action=..., graphql-inspect target=... are one tool with many behaviours, routed by a parameter. The project prefers extending those over adding new visible tools, the visible surface is deliberately small.

Tools live in the package itself (src/Mcp/Tool/), so this is a contribution to Bricklayer: fork, branch, PR. The good news is that registration is attribute-driven, there is almost no wiring.

Adding a tool

  • Pick the class for the domain in src/Mcp/Tool/ (CatalogTools, OrderTools…) and add a public method returning array, with the #[McpTool] attribute. Ship it hidden (tier 2) by default:
#[McpTool(
    name: 'product-archive',
    description: 'Archive a product by SKU. Use when a product should disappear from the storefront without deleting it.',
    meta: ['hidden' => true]
)]
public function archiveProduct(string $sku): array
{
    if ($error = $this->requireMagento()) return $error;
    if ($error = $this->requireToolEnabled('product-archive')) return $error;
    if ($error = $this->requireNonProduction('product-archive')) return $error;

    // ...
}
  • Keep the description under ~80 words and lead with when to use it. Agents reliably reach only for tools whose description tells them when.

Guards, in this order

Tool kind Guards
pure read/introspection requireMagento() only
write or cost-sensitive + requireToolEnabled('name')
destructive (deletes, refunds, codegen) + requireNonProduction('name'), and add the name to ConfigInitializer::DESTRUCTIVE_TOOLS so it ships disabled

Adding the requireToolEnabled() call is all it takes to make the tool configurable. init finds it by scanning for call sites, so it appears in .bricklayer.json and config:set without touching any list.

Traits

Use the shared traits from src/Mcp/Tool/Concern/ instead of reimplementing:

  • RequiresMagento - bootstrap guard and auto-reinit
  • ChecksConfig - the config and production guards
  • RespondsWithErrors - the uniform error shape
  • PaginatesResults - list envelope with has_more
  • FiltersFields - optional fields parameter
  • RequiresValidVerbosity - minimal/standard/detailed
  • MasksSensitiveConfig - whenever output could contain credentials

A new tool class

Create e.g. src/Mcp/Tool/CmsTools.php. Any class with #[McpTool] methods is picked up automatically and gets its own docs section. One manual step: register it in ToolRegistry::TOOL_GROUPS, otherwise its tools land in group other and search-tools grouping suffers.

Write a test under the mirrored tests/Unit/Tool/ path.

Guidelines, skills, categories

  • guideline: drop config/guidelines/{category}/{name}.md, served as magento://guidelines/{category}/{name}
  • skill: config/skills/{name}/SKILL.md, served as magento://skills/{name}
  • development-context category: one entry in ContextTools::CATEGORY_MAP with skills, guidelines, description and group

Regenerate and contribute

Agent files are built from the source, so after adding a tool regenerate them in Your test project:

vendor/bin/bricklayer update

MCP resources (guidelines, skills, templates) are served dynamically on each request, no build step for those.

Before opening the PR:

  • vendor/bin/bricklayer verify - the MCP tool count should go up by one
  • call the tool by name from Your agent; hidden tools do not appear in tools/list, find it with search-tools query="archive"
  • default to read-only wherever possible
  • think twice before asking for tier-1 visibility, the PR has to make the case

One thing not to “fix”: requireToolEnabled() fails open (a broken config must not brick read-only tooling), isProductionMode() fails closed (destructive tools stay blocked when the deploy mode is unknown). That asymmetry is deliberate.

Start coding!

Source: GitHub · CONTRIBUTING.md · SKILL.md