Skip to content

[5.x] Support categories, tags, and global sets in elements_* MCP tools - #20469

Open
brandonkelly wants to merge 2 commits into
5.xfrom
feature/mcp-category-tag-global-adapters
Open

brandonkelly wants to merge 2 commits into
5.xfrom
feature/mcp-category-tag-global-adapters

Conversation

@brandonkelly

@brandonkelly brandonkelly commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

Summary

The elements_* tools rejected categories, tags, and global sets: Unsupported element type [craft\elements\Category]. Supported types: entries, assets, users, addresses. Craft 5 sites still use all three heavily. On craftcom, for example, the Blog's postCategory field relates to categories, so agents could only refer to them by raw ID.

 ElementAdapterRegistry defaults
   EntryAdapter, AssetAdapter, UserAdapter, AddressAdapter
+  CategoryAdapter    categories   list/get/create/update/delete/duplicate (copy); criteria group, groupId, slug; context {groupId, siteId}
+  TagAdapter         tags         list/get/create/update/delete; criteria group, groupId; context {groupId}
+  GlobalSetAdapter   globalSets   list/get/update content only; criteria handle

Each adapter uses Craft's own element permissions (saveCategories:<uid> and so on), plus the editSite:<uid> check the categories and globals controllers make on multi-site installs, for saving and for deleting categories. Like entries, categories load by ID or UID even when disabled, including saved drafts; tags load even when disabled. Tags allow everyone, which matches Tag::canSave()/canDelete().

Global sets need an extra rule. GlobalSet::canSave() returns true for every user, because GlobalsController enforces editGlobalSet:<uid> itself. elements_restore and elements_validate find elements through ElementLifecycle with plain Authorization checks, so the rule lives in Authorization rather than only in the adapter:

Authorization::allows(user, ability, globalSet)
  if ability != view and (set is trashed or user lacks editGlobalSet:<uid>)
    deny
  else native canX()

Deleted global sets are config-backed. They come back through project config, so restoring the element by itself would leave an orphan, and elements_restore refuses it even for admins. Creating, renaming, and deleting sets is left to Settings → Globals, and elements_schema notes say so.

Categories and tags are now summarized as {type, id, status} in nested dot-path output, like other fetchable types. Before, they were fully serialized.

Craft 6: not applicable. In craftcms/cms 6.x, categories, tags, and global sets exist only in yii2-adapter/legacy, and the Craft 6 MCP server has no adapters for them.

Evidence

New ElementAdaptersTest:

categories: elements_create {groupId, title: "Releases"} -> elements_list {group: mcpTopics} ids == [it]
            disable it -> elements_get title == "Releases"
tags:       elements_create {groupId, title: "craft6"}   -> elements_list {groupId} titles == ["craft6"]
globalSets: user without editGlobalSet: elements_update and elements_get fail
            with editGlobalSet:<uid>:    elements_update sets plainTextField3
            admin: elements_restore on a deleted set fails
  • Before: ToolCallException: Unsupported element type [craft\elements\Category]. Supported types: entries, assets, users, addresses. (and the same for globalSets)
    After: OK (2 tests, 10 assertions)
  • Mutation checks: without the Authorization rule, a user without permission can update the global set. Without the trashed check, an admin can restore a deleted set. The test fails in both cases.

vendor/bin/codecept run unit mcp passes (72 tests). ECS, Rector, and PHPStan are clean.

Adds element adapters for categories (read, create, update, delete,
duplicate), tags (read, create, update, delete), and global sets (read and
update content only). Global sets' canSave() allows everyone, so MCP
authorization now requires editGlobalSet for anything but viewing them,
as the globals controller does, and never allows restoring deleted sets,
which come back through project config.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-c93e4e62-8dbe-4153-b265-a57d6fb57ac8
Copilot AI balanced review requested due to automatic review settings November 7, 2026 22:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Category deletion lacks a site-access check, allowing users with group delete permission to delete or hard-delete categories in sites they cannot access.

3 open issues
What changed in this PR

Extends Craft 5’s elements_* MCP tools to support categories, tags, and global sets through the existing adapter system.

Changes:

  • Registers category, tag, and global-set adapters with type-specific schemas and operations.
  • Restricts global-set changes to permitted content updates and blocks restoration of deleted sets.
  • Adds integration coverage and changelog entries for the new support.
File Description
tests/​unit/​mcp/​ElementAdaptersTest.php Tests category/tag operations and global-set permission restrictions.
src/​mcp/​elements/​ElementAdapterRegistry.php Registers the three new adapters.
src/​mcp/​elements/​adapters/​TagAdapter.php Adds tag criteria, attribute schemas, and field-schema context.
src/​mcp/​elements/​adapters/​GlobalSetAdapter.php Supports global-set content updates with site authorization.
src/​mcp/​elements/​adapters/​CategoryAdapter.php Adds category operations, copy duplication, and site authorization.
src/​mcp/​Authorization.php Enforces global-set edit permissions and blocks trashed-set mutations.
CHANGELOG.md Documents the expanded MCP support and new adapter classes.

Review effort: Balanced

return $this->lifecycle->duplicate($element);
}

/** @param Category $element */

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c3ac2a3: CategoryAdapter::canDelete() now requires editSite for the category's site on multi-site installs, as well as the native delete permission. This matches CategoriesController::_enforceEditCategoryPermissions(). One correction: Category::canView() doesn't check site access, so the check is explicit rather than going through canView().

return Category::class;
}

protected function criteriaProperties(): array

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c3ac2a3: categories now load with drafts(null)->status(null), as EntryAdapter does, and tags with status(null). Listing defaults are unchanged. The test now disables the category and reads it back. It fails without this change ("Category not found.").

- Load disabled categories, tags, and saved category drafts by ID or UID
- Require editSite to delete categories, as the control panel does
- Accept context.siteId for new categories' field schemas

Amp-Thread-ID: https://ampcode.com/threads/T-c93e4e62-8dbe-4153-b265-a57d6fb57ac8
Co-authored-by: Amp <amp@ampcode.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants