Craft CMS MCP
Version 1.4.0
August 28, 2026
The release that rebuilt how content is read and written. Relations, assets, categories and users are addressed by what they are rather than by numeric id, the read format and the write format became the same format, and every write lands as a reviewable draft by default. The thirteen beta sections below hold the detail; this section is the whole line read as one release.
Added
- Two editions. Everything that reads, inspects or queries the install is Lite; the eight tools that write entry content (
create_entry,update_entry,publish_entry,delete_entry,duplicate_entry,copy_entry_to_site,create_nested_entry,move_nested_entry) are Pro. On Lite those tools are not advertised at all, and the server instructions gain a section naming them and withdrawing the write guidance, so an agent is never taught a workflow the install cannot perform. See Editions showLockedProToolsconfig setting: keep the Pro tools listed on a Lite install, marked[Pro]and answering any call with an upgrade message, instead of hiding them. Off by defaultlist_mcp_toolsreportsrequiredEditionandlockedper tool, so a client can build an upgrade prompt without hard-coding which tools are paid- A plugin registering its own tools can require an edition with
#[RequiresEdition], on a method or a whole class, with the method-level attribute taking precedence - The payload format: one shape for reading and writing an entry, with relations as natural keys (
{"section","slug"},{"volume","filename"},{"group","slug"},{"username"}) and Matrix blocks as keyed objects. Whatget_entryreturns is whatcreate_entryandupdate_entryaccept, so an agent can read an entry, change one value, and write it back without translating anything - Draft-first writing. Every write lands as a draft carrying a control panel link for review, and nothing reaches live content until
publish_entryapplies it.list_draftsis the review queue andlist_revisionsis the history - Single Matrix block editing:
create_nested_entryadds one block without resending its siblings,move_nested_entryrepositions one, and a block's own id works withupdate_entryanddelete_entry. Rewriting a whole field value to change one block no longer deletes the blocks left out of the payload count_entries: totals and per-value breakdowns by attribute, by date bucket, or by field value, answering "how many per X" in one call instead of listing everything- Multi-site content:
list_sites,get_site,list_site_groups, andcopy_entry_to_sitefor translation. Writes that reach further than the site named in the call reportaffectedSites, the sites they actually landed on - MCP token management in the control panel, with readonly, content and full scopes per user, plus
disabledScopesto block minting a scope per environment. A team can share one install without sharing one key get_mcp_infoandlist_mcp_toolsreport the connection they are answering on: transport, scope, user, and how many tools this connection can actually call rather than how many are registeredmaxResponseBytesconfig setting: refuse a single tool result past this size instead of emitting one nothing downstream can read. Defaults to 1 MB, and0disables the check
Changed
list_entriesandcount_entriescover top-level entries. A section-less read no longer mixes in nested Matrix blocks, which belong to no section; passincludeNested: truefor the previous result set. Both tools share one scope, so their totals agree- Every tool taking a
limitreturns the same envelope:count,total,limit,offsetwhere the tool takes one, andhasMore. Where a tool cannot count without repeating the read,totalandhasMorearenullrather than inferred from the page size get_graphql_schemareturns a type index by default, withtypeandfieldparameters to widen it. A Craft schema prints to hundreds of kilobytes, so the index carries each type's printed size and the narrowed views stay inside one responseget_configreports a placeholder in place of a credential, matching the config resource, and refuses an unknown category or setting instead of answering- Arguments no tool declares are refused rather than dropped. A misspelled parameter name used to be ignored, and the tool answered as though it had never been passed
list_categoriesandlist_usersacceptoffset, so a caller told there is more can ask for itdescribe_entry_schemareports the sources a relation accepts as natural key parts rather than uids, with an explicitunresolvedentry for a source naming nothing on the install- Page bounds are published on the schema and enforced on every tool taking
limitoroffset, so a client can check before it calls - Requires
craftcms/cms^5.1andmcp/sdk^0.7
Fixed
- The stdio server no longer stalls against a client that sends several calls at once. A response larger than the socket buffer is delivered while the server keeps reading, and a request split across writes is reassembled before dispatch
read_logsandget_last_errorsearch until they have enough matches instead of reading a fixed window from the end of each file, so a smalllimitno longer reports an install as error-freeget_deprecationsreads Craft's rotated log files, and reports only deprecationsrun_queryapplies its row cap however the statement is written, and judges a keyword by whether it is SQL rather than by whether the word appears, so a search for the wordupdateis a readlist_draftsrefuses an unknown section handle by name instead of returning an empty queuecount_entriesgrouped by site counts every site rather than naming the one the query already ran against- Date filters refuse a bound that names no instant instead of quietly filtering on the moment of the call
- Ambiguous natural keys are refused with the ambiguity named, rather than resolved to whichever element matched first
- A revision or draft id passed to a tool that works on canonical entries is refused with the canonical id to call instead
- Placeholder slugs are cleared after a nested block is saved, so a block does not keep an internal temporary value as its slug
Version 1.4.0-beta.13
August 18, 2026
Added
get_mcp_inforeports the connection it is answering on: transport, scope, user, and whether the connection may read install internals. An agent had no way to ask what it was allowed to do, so it found out by being refusedget_mcp_infoandlist_mcp_toolsreport how many tools this connection can actually call, not just how many are registered. On a scoped connection those are different numbers, and only one of them is usefulget_mcp_infotakesdetail, which adds the reason each unavailable tool is unavailable, the text of any registration errors, and Craft and PHP versions. Off by default, since this is often the first call of a session. The install facts are withheld from a connection that may not read install internals, and say so rather than going quietget_mcp_inforeportsbuildSourceand, on a working copy,branch. The build reference is now read from the checked-out commit where there is one, falling back to what Composer recorded- Write operations that reach further than the site named in the call now say so in their response.
publish_entry,delete_entry,create_nested_entryandmove_nested_entryreturnaffectedSites, the site handles the operation actually landed on. Publishing applies a draft to every site it exists on, deleting trashes the entry on every site, and a nested block's presence and position are shared becauseelements_ownershas no site column; in each case thesiteargument only chooses which row is located. A description helps whoever read it, and a response field helps every caller and cannot drift out of date describe_entry_schemareports each field's translation method on a multi-site install, and whether the field holds a separate value per site. That is the question an agent has before writing: setting a non-translatable field on one site changes it on all of them, and nothing in the schema said so. Omitted on single-site installs, where there is no other site to leak into- Every tool carries a human
title, and the parameters whose meaning is not obvious from their name carry descriptions, so a client rendering a tool picker shows something readable and an agent stops guessing at arguments - Prompt arguments carry descriptions too, and
craft://guides/content-writingis annotated as the resource an agent should read before writing, in the protocol's own vocabulary rather than only in prose resources/subscribeon a resource template now works instead of succeeding and then going silent forever: the subscription is matched against the concrete URIs the notifier produces- A tool class carrying
#[McpTool]at class level is discovered and dispatched through__invoke, which is the SDK's own documented pattern and previously produced nothing - An external tool's
annotations,title,icons,metaandoutputSchemasurvive registration instead of being dropped, so a third-party read-only tool is no longer advertised with the conservative destructive defaults addDiscoveryPath()does whatdocs/extending.mdsays it does. It was stored and read by nothing, so a plugin registering a discovery path got no tools and no error- Text output for tool payloads:
run_query,get_table_counts, andcount_entriesacceptoutput="text"and return the same data laid out for a person (uniform rows as an aligned table, key-value data and breakdowns as aligned blocks, nesting indented), falling back to pretty JSON for anything that will not lay out cleanly. A tool opts in purely by declaring the parameter in its signature, and the rendering happens centrally, so no tool carries formatting code of its own colorOutputconfig setting: whether human-readable output carries ANSI colour. Off by default, because escape sequences reach an AI client as literal noise it pays tokens forget_mcp_inforeports the commit the running code came from, alongside the version. A release install reports a tag and the version is enough; a branch install reports the constraint (dev-main) for every commit that branch will ever have, so two deploys a week apart were indistinguishable and a stale server looked exactly like a fresh one
Changed
move_nested_entryandcreate_nested_entrydescribe what theirsiteargument does. Reordering is not per-site: a block's position is shared, so a reorder on one site becomes the order on every site, which was verified by moving a block on a second site and reading the first back changedpublish_entryanddelete_entrynow describe what theirsiteargument actually does. Neither operation is per-site: publishing applies the draft to every site it exists on, and deleting trashes the entry on every site at once, so the argument only chooses which row is located and which site's values come back. The old descriptions read as though either could be scoped to one translationread_logswithoutput="text"is no longer coloured unconditionally; its layout is unchanged andcolorOutputrestores the colours. Colour is now decided in exactly one place instead of per formatter- Tool annotations describe what each tool actually does.
create_entry,duplicate_entry,copy_entry_to_siteandcreate_nested_entryadvertiseddestructiveHint: truewhile being purely additive, andrun_queryadvertised it while being SELECT-only underSqlReadGuard;move_nested_entrygainsidempotentHint: true, since moving a block to position N twice leaves the same state, andreload_mcpgains annotations at all instead of inheriting the spec's conservative defaults for a call that changes no data. Every tool now also declaresopenWorldHint: false, which is accurate for a server that only ever touches one Craft install. None of this changes what a token may call: access is gated by thedangerousflag in our own metadata, and these hints only tell a client when to ask for confirmation. Prompting on read-only and additive calls trains people to click through the prompts that matter
Fixed
list_mcp_toolsno longer contradictstools/list. It computed each row's availability from the settings alone, which is one of three reasons a tool can be unavailable, so a readonly connection was shown 55 tools of which only 42 could be called, with the other 13 marked enabled. It now asks the same gate the server filtered the registry with, and carries that gate's reason on every refusal- The reported build is no longer whatever Composer last recorded. A path or symlink install, which is how this plugin is developed and how a monorepo deploys it, runs code Composer never re-read, so the recorded reference could name a branch that was not checked out. The commit is read from the working copy when there is one
- An ambiguous natural key no longer resolves to an arbitrary element. A slug is unique per site, not per section, so in a structure section the same slug under two parents is ordinary content modelling; the resolver returned whichever row the database handed back first, and a relation, a link, a
relatedTofilter or aparentcould silently address the wrong entry while the response reported no warnings. All of them now refuse the key and report that it matched more than one, which is what the documented contract ("natural keys become warnings, never guesses") always promised - A write the tool itself refused now reaches the client flagged as an error.
create_entry,update_entry,duplicate_entry,copy_entry_to_siteand one path increate_nested_entryreturned a payload whose JSON saidsuccess: falseinside an otherwise successful call, so nothing told the model to correct itself - Static and inherited methods are no longer advertised as tools. They appeared in
list_mcp_toolsas enabled and answered METHOD_NOT_FOUND when called, because the SDK's discoverer skips both and ours did not Psr11ContainerAdapter::has()no longer answers for Craft's whole service locator, where a tool class named after a component id would have resolved to the component- The discovery cache turns over on a branch install. Outside devMode the cache key was the plugin version, which never changes while a branch's code does, so the first deploy's tool scan would be served indefinitely. The key now carries the installed commit as well
- A tool that declares an
outputSchemanow receives the matchingstructuredContent. The duplicate is still dropped for every tool that declares none, which is all of them today, so nothing changes on the wire; without the guard, the first tool to declare a schema would have advertised it and silently never honoured it - Every tool, prompt and resource body is now just its own work. The wrapper each one opened to convert exceptions and run the freshness probe is gone: both jobs moved to decorators that cover every call, so 78 closures and three helper classes came out with no change to what any tool returns. An architecture test keeps them from creeping back
- An error thrown outside a tool body keeps its message. Anything that failed during argument preparation, result formatting, or inside the output layer itself reached the SDK's generic catch and came back as the fixed string
Error while executing tool, with the real cause discarded and nothing for an agent to act on. That is not hypothetical: the fatal that broke every parameterless tool on this branch surfaced exactly that way. A caller mistake keeps its own wording, because "Missing required argumentid" is more use than the same text behind a file and line number - The project-config freshness probe can finally notify. It only tells subscribers when handed a request context, and 4 of 62 call sites passed one, so it ran on every tool call and could reach nobody. It now runs once per tool call with the context the SDK already injects
- The server advertises only the capabilities it honours. Left to the SDK's inference, every
listChangedflag was on purely because an event dispatcher existed, so a client that trusted them waited for notifications nothing ever sent. Only the tool list has a producer (reload_mcp, after a rescan); prompts and resources now say so, and a client is told to poll rather than told to wait. Subscriptions and client logging stay advertised, because they now actually work on both transports - The server survives the SIGHUP restart it tells agents to use.
reload_mcp's hint text and description both point at that signal, and following it silently ended the session: the SDK closes the transport when the run loop exits, which closes file descriptors 0 and 1, and the restart then replaces the process image with those descriptors already closed, so the new server could neither read a request nor write a reply and the client hung until it timed out - A second request arriving on stdio while one is still in flight no longer discards the first. The transport holds a single fiber slot, so the new request overwrote the pending one, which was then never answered and left its caller blocking with nothing logged. The new line is simply left unread until the fiber finishes, which is correct backpressure on a non-blocking stream; a fiber waiting on the client is deliberately exempt, since its reply arrives through the same path
- The HTTP endpoint wires the same container as stdio. It registered only a logger, so the SDK's bare-instantiation fallback fired for every tool with a dependency:
tinkerran with a null logger and wrote nothing, and the elements layer fell back to the default translators, which meant a plugin registering a custom field translator saw its field types translated on stdio and raw over HTTP. The same entry, read through two transports, came back differently - The server answers
initializewith the protocol revision it actually implements, instead of whatever the SDK was compiled against, and the HTTP endpoint no longer admits2024-11-05, a revision that predates the transport it would arrive on - Response headers cross to Craft one line per value rather than comma-joined. Latent today, because no SDK response repeats a header, and silently wrong the moment one does:
Set-Cookiemust never be folded - The CORS middleware is no longer installed. The controller answers
OPTIONSitself before any middleware runs, so its preflight branch was unreachable, and an installed-but-unreachable guard reads as protection that is not there. Browser clients need both halves changed together, which is a deliberate decision rather than an oversight - Tools that notify work over HTTP. Any tool that sends the client a notification suspends a fiber, and the SDK answers that by switching the whole response to a server-sent event stream whose frames are echoed from a lazy callback, after the controller has closed its output buffer. The frames reached the SAPI directly, PHP sent its own headers first, the
text/event-streamcontent type never arrived and Yii then died on headers-already-sent, so the client received event-stream framing labelled as HTML.reload_mcphit this on every HTTP call; a live-mode write hit it whenever a session had subscribed to the entry. The fiber is now drained in the request that started it and everything it produced is returned as one JSON-RPC batch, which is the same shape the SDK emits when no notification is involved. Nothing streams under FPM, so nothing is lost. A tool that waits on a reply from the client (elicitation, sampling) now fails immediately with a reason instead of pinning a worker until the two-minute timeout move_nested_entryworks on an owner that carries a block Craft would now reject. Reordering re-saves the owner, which revalidated every sibling, so one block whose entry type had since been removed from the field made the whole field unreorderable, and the outcome depended on whether the call happened to open a fresh draft or reuse an existing one. A reorder changes no value, only the order of children that are already stored, so it no longer validates on the way throughcreate_entryandupdate_entryacceptpostDateandexpiryDate. They were the only writable attributesdescribe_entry_schemaadvertised that no tool could actually take, so the schema tool documented a payload the write tools rejected. A bare timestamp is read in the site's timezone, matching whatget_entryprints, so reading an entry and writing it back no longer shifts its dates by the site's offset on every round tripdescribe_entry_schemano longer advertises attributes that cannot be sent. Its writable list is inferred from Craft, which callssectionId,typeId,ownerId,sortOrderand friends safe; they are addressed through named tool arguments or not at all, so listing them told the agent it could send keys the write tools would rejectdescribe_entry_schemaclampsdepth. Each level expands every block type of every Matrix field, so an unbounded value made the server do multiplicative work to produce a response too large to useduplicate_entryreports the warnings from its own write. Supplyingfieldscould drop an unresolvable relation and still return plain success, so the caller could not tell a complete duplicate from a partial one. It was the only write path that did this- A relation that cannot be expressed as a natural key now says so. The read path falls back to the raw id, which is correct, but did it silently, so a payload that will not round-trip looked identical to one that will
expectedDateUpdatedcompares the entry you addressed, not its canonical. Writes return a derivative draft's id andget_entryon that id reports the draft's own timestamp, so passing it back could never match and the tool told the agent to re-read and retry, which produced the same timestamp and the same refusal. It also watched the wrong thing in both directions, missing a concurrent edit of the draft while blocking on canonical changes that could not conflict- A Matrix payload that carries
positionon only some blocks is ordered rather than ignored. Sorting was skipped entirely unless every block had one, which is exactly the payload produced by reading a field and adding a block to it, and the field was then silently reshuffled into transport order - Natural keys resolve against unpublished drafts, in both directions. Writes land as drafts by default, so creating an entry and relating to it from the next one is the ordinary session, but relation lookups inherited Craft's default of excluding drafts entirely. The relation was dropped with "No entry matches this key" for an entry the same agent had created moments earlier, and an entry that legitimately related to an unpublished draft read back with a bare integer id instead of a key. Only unpublished drafts are admitted: publishing one keeps its element id, so a stored relation survives, whereas a derivative draft is hard deleted on publish and would take the relation with it (and shares its canonical's slug, so admitting it would make the result order-dependent too)
describe_entry_schemaadvertises thepositionkey on Matrix blocks. The read path stamps it onto every block and the write path honours it, but the schema tool did not mention it, so the tool an agent is told to read first was wrong about the one key that controls block ordertinker'soutputparameter advertises its allowed values. It carried aCompletionProviderattribute, which MCP only honours for prompts and resource templates, never for tool arguments, so it was dead code standing in for schema information: the generated schema offered neither the four render modes nor a description, and an unrecognised value silently becamedump. The parameter is now typed as its enum, so the schema carriesdump,json,rawandprint_r- An external tool declaring
#[McpTool(description: ...)]without aname:registered under the empty string. The SDK falls back to the method name and so do our prompt and resource events; only the tool event did not, so such a tool reachedtools/listunnamed, uncallable, and colliding with any other tool that made the same omission - An attribute subclassing
McpTool,McpPromptorMcpResourceis discovered again. Every SDK lookup passesIS_INSTANCEOFand none of our fourteen did, so the SDK registered such a tool and our own deny-by-default sweep then removed it, leaving only a debug-level trace. Subclassing is legal and is a normal way to carry house metadata - A tool class implementing
ConditionalProviderhad itsisAvailable()ignored, because tool registration tested the deprecatedConditionalToolProvidersubinterface while prompt and resource registration tested the base one. All three now agree; the deprecated interface keeps working, since it extends the base query_graphqlandexecute_graphqlwork over stdio. Craft's GraphQL execution is console-safe on its own, but it fires events, and a plugin listening on one of them may ask the current request for headers the way it would in a web context. Over stdio that request is a console request, which has no such method, so both tools died with an unhelpful "Calling unknown method" and the agent was told nothing it could act on. The console request now carries a behaviour answering that one question with an empty header collection, which is the honest answer and the same branch a listener takes on a web request that sent no header. Craft's own console checks are unaffected- Tools that take no arguments no longer fail with
-32603 Error while executing tool. Nineteen tools on a stock install take none, and the SDK hands their schema over with the property map as an empty JSON object rather than an array; the output layer read it as an array and died on every call, takingreload_mcp,get_mcp_info,list_sitesandlist_pluginswith it - Logged exceptions are readable. A
Throwablecarries no public properties, so encoding the conventional['exception' => $e]context turned every one of them into{}, and every error this server had ever logged was undiagnosable. The context is now normalized first: class, message, code, file, line and a trimmed stack trace, along the wholepreviouschain, bounded so that one entry still occupies one line and a deep trace cannot flood the file - Cached tool discovery invalidates itself when the scanned code changes. Editing a tool, adding a tool class or switching branches kept
tools/listserving the previous scan, and neither reconnecting the client nor restarting the container helped, because the staleness lives in Craft's cache rather than in the process. The cache key now carries the state of the code it was built from: a stat-based fingerprint of the plugin source whendevModeis on, and the plugin version otherwise, so production keeps a stable key and does no filesystem work per request - Every tool payload is sent once instead of twice. The SDK serialized array returns into both
contentandstructuredContent, doubling the wire and token cost of every call; no tool declares an output schema, so the duplicate carried nothing a client could use
Removed
- Undocumented introspection helpers with no callers anywhere:
Mcp::collectExternalTools(),collectExternalPrompts()andcollectExternalResources()(deprecated in favour of the registries),getAllToolClasses()and its prompt and resource twins on the registration events,ToolRegistry::getToolClasses()andgetToolsBySource(),PromptRegistry::getPromptClasses(),ResourceRegistry::getResourceClasses(), and the six copies ofgetDefinitionsByCategory(). None appear indocs/extending.md, so none were part of the documented extension API.getDefinitionsBySource(), which is used, stays
Version 1.4.0-beta.12
August 6, 2026
Fixed
disabledScopesis now enforced at authentication too: a token minted before its scope was disabled stops working (rejected exactly like an unknown token, leaving no trace on the token) until the scope is re-enabled, which restores it without recreation. Previously the setting only gated new minting, so a pre-existing token of a disabled scope kept working, exactly the loophole the guardrail exists to close, as argued in #56 (#48, #56)
Version 1.4.0-beta.11
August 6, 2026
Added
showClientConfigSnippetconfig setting: when off, the ready-to-paste Claude Desktop config block is not built or shown on the control panel token-reveal screen, and themcp/tokens/createconsole command prints the plaintext token without it; the setting is consulted in exactly one place, so the two surfaces can never disagree (#62)
Fixed
- Entries created by
create_entryin draft mode now land as saved drafts, visible in the control panel Entries list, search, and the Drafts status filter for every user with access, exactly where a human's "+ New entry" then "Save draft" lands. Previously they stayed in Craft's "still being composed" autosave state forever: invisible to reviewers everywhere except the creator's own dashboard widget, and subject to Craft's stale unsaved-draft garbage collection. They remain unpublished drafts either way (no public URL untilpublish_entry). Drafts created by earlier versions stay in the old state; a no-opupdate_entryon such a draft's element id flips it to saved with no content changes (#61)
Version 1.4.0-beta.10
August 6, 2026
Added
disabledScopesconfig setting: block minting specific token scopes per environment (for example'production' => ['disabledScopes' => ['full']]), enforced for everyone including admins, in the control panel dropdown, the control panel create action, and the console command through one shared predicate; existing tokens of a disabled scope keep working and keep regenerating (#48)additionalInstructionsconfig setting: install-owner text appended to the end of the server instructions on every transport, for deployment-specific guidance an agent must see before its first write (house style guides, staging warnings, approval workflows); empty by default (#52)
Changed
craftcms/cmsrequirement raised to^5.1: the control panel token screens useEditUserTrait(since Craft 5.1), which is a compile-time fatal on Craft 5.0 (#51)- CI now tests PHP 8.3, 8.4, and 8.5, PHPStan analyses against that full version range, and lefthook pre-commit/pre-push hooks wire themselves via
composer installfor contributors;rector/rectorfloats at^2.5so it tracks PHPStan releases
Fixed
- Tools registered by SDK attribute discovery without being available (for example the Commerce tools when Craft Commerce is not installed) no longer bypass
disabledTools, scope, and the privileged axis: anything without an informational definition is unregistered deny-by-default, andget_mcp_info'sbySourcecounts now sum to itstotal(#57) disabledPromptsanddisabledResourcesare now actually enforced at serve time, with the same deny-by-default sweep as tools; they were documented but never applied. The content-writing guide resource is also registered in the informational registry, where it had been missingcraft://entries/{section}/statsis reachable again: it registered after the{section}/{slug}template, whose variable segment swallowed the literalstats(#49)describe_entry_schemaexpands each Matrix field once instead of twice:input.blockTypesis now a flat map of the valid block-type handles and the field's top-levelblockTypeslist is the single depth-expanded representation, keeping per-fieldinstructions; nested Matrix fields at exhausted depth now name their block types too (#53)- The server instructions no longer recommend tools a connection cannot call: a computed availability note lists any cited-but-disabled tools, and the full-scope wording is qualified to what the install exposes (#50)
- The control panel no longer offers the Full scope to token managers who are not admins only to reject it on submit: the dropdown and the create authorization consult the same predicate (#48)
Version 1.4.0-beta.9
July 20, 2026
Changed
- Upgraded
mcp/sdkfrom^0.6to^0.7. Scope-based tool filtering keeps working under the SDK's new deferred element loading, since a registry supplied viasetRegistry()is loaded eagerly at build time; a test pins that contract so a future SDK change that breaks it fails loudly. - Upgraded
rector/rectorfrom2.5.5to2.5.7.
Version 1.4.0-beta.8
July 20, 2026
Fixed
- The HTTP endpoint now works on installs that serve the control panel from the root of its domain (
cpTriggernull): the endpoint registers as a CP URL rule there, since such hosts never consult site URL rules. Craft intercepts the defaultmcppath for guests on those hosts (it matches the plugin handle, a Craft core behavior that runs before routing), so a differenthttpPath(for examplemcp-http) is required and the new "Control panel on the root domain" section in the HTTP transport guide explains the setup.
Version 1.4.0-beta.7
July 17, 2026
Added
- Manage MCP tokens from the control panel: an "MCP Tokens" screen on My Account (mint readonly/content tokens for yourself, view, regenerate, and revoke your own), the same screen on other users' pages for token managers, and a Utilities panel listing every token. New tokens reveal once in a modal with copy and download buttons; regenerate re-issues a token in place (fresh secret, same name/scope/user/expiry, old one invalidated). Gated by two new user permissions; visible only when
httpTransportis enabled. - readonly and content HTTP tokens now read only what their linked Craft user may view: entries, assets, and categories are filtered to the sections, volumes, and groups the user can view, and user listing requires the View Users permission
- Install-introspection and unbounded-read tools (read_logs, get_last_error, get_config, get_database_schema/info, get_table_counts, get_project_config_diff, get_environment, query_graphql, list_graphql_tokens, list_globals, list_backups, and the Commerce order/product reads) are locked to admins over read/content-scope tokens; open specific ones with the new
scopedTokenPrivilegedToolsconfig setting paginationLimitsetting for the MCP list page size, defaulting to 100 so a single unpaginatedtools/listcall sees every registered tool (#40)- HTTP transport sessions are stored in the database (new
mcp_sessionstable), so they survive load-balanced, multi-instance hosting; the newhttpSessionStoresetting accepts a class name or callable for custom stores such as Redis (#41) - Attribute discovery is cached through Craft's cache component, so HTTP requests skip the full per-request reflection scan;
reload_mcpinvalidates the cache - The tinker, database, and debug tools stream diagnostics to the client at the log level it negotiates via
logging/setLevel - Resource subscriptions now deliver: subscribed clients receive
resources/updatedwhen a live write or publish changes an entry, or when an external project config change refreshes the config resources, andreload_mcppushestools/list_changed - The tool registry is wired to a real event dispatcher, so the list-changed server capabilities advertise truthfully
- Documented that Matrix-family blocks are directly addressable by their own id in
update_entry,delete_entry, andpublish_entry(#39)
Fixed
- Long-lived MCP processes now pick up custom fields added by another process (for example
project-config/apply): the config freshness probe patches the compiledCustomFieldBehaviorhandle map and flushes the GraphQL schema caches instead of leaving the process stale until restart - MCP resource reads now respect the same authorization as tools: entry resources (
craft://entries/...) are bounded by the acting user's view permissions and config resources (craft://config/...) are admin-only over readonly and content-scope tokens, closing a read path that previously bypassed the tool-surface guarantees entirely
Version 1.4.0-beta.6
July 15, 2026
Added
- Content-scope HTTP tokens now respect the linked user's real Craft permissions on entry writes (create, update, publish, delete, duplicate, copy to site), checked live per request through Craft's element authorization (#34)
- Publishing mirrors the control panel's apply-draft gate: the acting user must be allowed to save the draft itself (own draft or peer-draft permission) and its canonical version, on both the draft-id and canonical-id publish routes
- Drafts created over the HTTP transport are attributed to the token's user, so editors can publish their own drafts without peer-draft permissions
Version 1.4.0-beta.5
July 14, 2026
Added
count_entriestool: totals and per-value breakdowns (attribute, date bucket, or field-value grouping) with the same filters aslist_entries- Field-value
filters,relatedTo,author, and date-range parameters onlist_entries, with natural keys and:empty:/:notempty:support fieldsprojection onlist_entriesfor slim rows when scanning many entrieslist_revisionstool: an entry's saved history (who, when, notes);get_entrynow reads revision idsquery_graphqltool: read-only GraphQL queries (mutations rejected at parse time), not gated byenableDangerousTools- Read-only and idempotent annotations on every non-dangerous tool (reload_mcp exempt)
- Tool-selection ladder in the server instructions, prompt guides, and the
tinker/run_querydescriptions
Version 1.4.0-beta.4
July 10, 2026
Fixed
- Production safety defaults (
enabledandenableDangerousToolsoff) now apply BEFORE the config file is read instead of only when no config file exists. Previously, creatingconfig/mcp.php(as every quick start instructs) silently re-enabled dangerous tools in production; now only an explicit'enableDangerousTools' => truein the file does that. config/mcp.phpnow supports Craft's multi-environment config convention (a'*'base merged with the current environment's block), which the configuration guide documented but the loader ignored.
Added
list_draftstool: the review queue for the draft-first workflow. Lists pending (non-provisional) entry drafts newest first, filterable by section, site, or creator, with the draft element id, canonical id, draft notes, and a control panel deep link per row. Available toreadonlyHTTP tokens, since reviewing is reading.review_pending_draftsprompt: walks the queue conversationally (inspect viaget_entry, approve viapublish_entry, reject viadelete_entry).
Version 1.4.0-beta.3
July 10, 2026
Changed
- Agent guidance now teaches the content-writing upgrade everywhere it speaks: the server instructions walk through schema discovery, natural keys, and the draft-first flow; the
create_entry_guideandbulk_entry_operationsprompts stop inviting payload guessing and lean ondescribe_entry_schemainput shapes and drafts-as-safety-net; and a newcraft://guides/content-writingresource serves the full payload contract on demand. HTTP connections additionally get a per-scope note in their instructions stating what the presenting token can and cannot do, and thedebug_content_issueprompt stops recommending a tool that does not exist.
Version 1.4.0-beta.2
July 9, 2026
Fixed
- The long-lived stdio server now detects external project config changes (migrations,
project-config/apply, control panel edits) before each tool call and refreshes itself, endingStaleResourceExceptionon writes and silently stale schema reads across the whole read surface (fields and layouts, sections and entry types, sites, volumes, category groups, global sets). No more manualreload_mcpafter running migrations. Thanks @dgaidula for the report and groundwork in #18.
Version 1.4.0-beta.1
July 9, 2026
Added
elementsmodule: element-generic payload engine (natural keys for relations, draft-first writes, schema discovery), reusable outside the plugin (imports only craftcms/cms and psr/log, enforced by an architecture test)describe_entry_schematool: fields, kinds, required flags, matrix block types, native fields, writable meta attributes, optional golden-fixtureexampledescribe_entry_schemanow returns a per-fieldinputshape describing the payload each field accepts: relations show their natural-key shape (e.g.{section, slug}), Matrix and Hyper-style fields recurse into their block/link types and sub-fields, core Link fields list their configured types and key shapes, option fields list allowed values, tables list their columns, scalars show their value type. Derived dynamically from field layouts and the module's own key contract, so it stays correct for any field including third-party ones.publish_entry,delete_entry,duplicate_entry,copy_entry_to_siteworkflow toolsentryWriteModesetting ('draft'default,'live'restores immediate saves); per-callmodeoverridesiteparameter on all entry tools (#9);searchonlist_entries,parenton the entry write toolssearch/typefilters onlist_fields,searchonlist_sectionsEVENT_REGISTER_FIELD_TRANSLATORSfor plugins whose field types embed element ids- HTTP transport (opt-in): serve the MCP server from a Craft endpoint with per-user scoped bearer tokens (
readonly/content/full), managed viacraft mcp/tokens/*console commands. Content editors can connect Claude Desktop to a remote install with no local tooling. (#13) - Dangerous tools now carry a
destructiveHintannotation intools/list, making risk visible to MCP clients.
Changed
get_entry/list_entriesreturn the payload format: relations as natural keys ({section,slug}, {volume,filename}, ...), matrix blocks in core shape with translated internals, disabled blocks included; what you read is what create/update acceptcreate_entry/update_entrysave as drafts by default (setentryWriteMode: 'live'or passmode: 'live'for the old behavior); validation failures return structured per-field errors instead of a JSON blob
Version 1.3.0
July 7, 2026
Added
logLevelconfiguration option to control MCP server log verbosity (storage/logs/mcp-server.log). Set to'debug'for full tool dispatch logging. Default:'error'(#10)- Debug logging in
tinkertool for execution tracing, security blocks, and error diagnostics
Fixed
tinkertool now wraps execution inSafeExecution, ensuring unexpected errors surface with real messages instead of the generic "Tool execution failed" (#10)- Stray output (echo/print/PHP notices from tinker'd code or Craft) no longer corrupts the stdio JSON-RPC stream; it is rerouted to stderr via a non-removable output buffer in
bin/mcp-server tinkertool blocks unboundedwhile (ob_get_level())teardown loops, which would spin forever against the non-removable stdout shieldtinkertool now drains only output buffers it opened (baseline-aware): the previousob_get_level() > 0guard closed outer buffers such as the stdout shield, clobbering the original error with anErrorExceptionwhen user code had closed the capture buffer- Added explicit
psy/psyshrequirement: tinker relied on it arriving transitively (e.g. viayiisoft/yii2-shell) and fatally errored on installs without it run_queryandexplain_queryno longer reject legitimate SELECTs whose columns contain a blocked keyword as a substring (e.g.dateCreatedmatchedCREATE,dateUpdatedmatchedUPDATE); the read-only guard now matches keywords on word boundaries via the sharedSqlReadGuard, andexplain_querygained the stronger keyword setget_deprecationsdatabase lookup selected a nonexistenttemplatecolumn, so the query always failed and was silently swallowed; the column is removed and the query now runsget_table_countsandget_deprecationscheck table existence via schema instead of catching everyThrowable, so a genuine database error surfaces instead of being masked as "Table not found" / zero resultslist_asset_foldersno longer dereferences a null root folder when a volume has no indexed root folderlist_backupsandcreate_backupguard againstfilesize()/filemtime()returningfalse(file removed mid-listing), preventing aTypeError- Added explicit
symfony/polyfill-php84requirement:array_any()(PHP 8.4) is used on the supported^8.3floor and previously relied on the polyfill arriving transitively list_event_handlersreported class-level handlers with theclassandeventfields swapped; Yii stores them as$_events[eventName][className], so the two nesting levels were mislabeled
Changed
- Extracted read-only SQL validation into a shared
SqlReadGuardsupport class used byrun_queryandexplain_query - Upgraded
mcp/sdkfrom^0.4to^0.6 - Added explicit
symfony/finderrequirement: the SDK moved it fromrequiretosuggestin 0.5, but file-based discovery (used for tool/prompt/resource registration) still needs it at server boot
Version 1.2.2
March 4, 2026
Fixed
tinkertool now releases mutex locks after execution, preventing project config deadlocks in long-running MCP process (#7)
Added
MutexGuardsupport class for releasing Yii2 mutex locks and resetting Craft's internal lock state
Version 1.2.1
February 24, 2026
Changed
- Upgraded
mcp/sdkfrom^0.3to^0.4
Version 1.2.0
February 24, 2026
Added
- Monorepo-aware project root detection in
mcp/installwizard- Auto-detects
.ddev/.gitmarkers in parent directories - Config files (
.mcp.json,.cursor/mcp.json) placed at actual project root - Binary paths automatically prefixed with Craft subdirectory (e.g.,
backend/vendor/...)
- Auto-detects
ProjectRootResolverclass for clean project root resolution (SRP)- Unit tests for
ProjectRootResolver
Changed
- Upgraded
mcp/sdkfrom^0.2to^0.3 - PHPStan ignore patterns updated for Craft CMS generic type annotations
Fixed
mcp/installnow writes config files to the correct location in monorepo setups- Removed dead code branch in
McpServerFactory
Version 1.1.1
January 7, 2026
Added
- Interactive
mcp/installwizard for generating MCP client configuration files- Auto-detects DDEV vs native PHP environment
- Supports Claude Code, Cursor, and Claude Desktop clients
- Options:
--environment(-e),--serverName(-s)
read_logstool now supportspatternparameter for case-insensitive content searchread_logstool now supportssourceparameter to target specific log files (web, console, queue, or plugin name)read_logstool now discovers plugin logs recursively in subdirectoriesread_logstool now parses multi-line stack traces into structured arraysread_logstool now sends progress notifications to clients while parsing multiple filesread_logstool now supportsoutputparameter (structuredortext) for human-readable colored output- New
ResponseFormatenum for reusable tool output format selection - New
LogParser,LogEntry,StackFrame, andLogFormatterclasses for clean log architecture
Changed
- Refactored log parsing into dedicated
LogParserclass for cleaner architecture FileHelper::tail()now uses chunk-based reading for improved performance on large files
Fixed
- Fixed null pointer errors in entry, category, and product serialization when related model (section/type/group) is missing
list_console_commandsnow discovers all commands including those from pluginstinkertool now properly preserves indentation in multi-line PHP input
Version 1.1.0
January 4, 2026
Added
- 18 new MCP tools expanding the toolset from 32 to 50 tools:
- Multi-site tools:
list_sites,get_site,list_site_groups - GraphQL tools:
list_graphql_schemas,get_graphql_schema,execute_graphql,list_graphql_tokens - Backup tools:
list_backups,create_backup - Self-awareness tools:
get_mcp_info,list_mcp_tools,reload_mcp - Commerce tools (Craft Commerce):
list_products,get_product,list_orders,get_order,list_order_statuses,list_product_types
- Multi-site tools:
- 9 MCP prompts for guided analysis workflows
- Content analysis:
content_health_analysis,content_audit,debug_content_issue - Entry management:
create_entry_guide,query_entries_guide,bulk_entry_operations - Schema exploration:
explore_section_schema,field_usage_analysis,explore_content_model
- Content analysis:
- 12 MCP resources for read-only URI-based data access
- Schema resources:
craft://schema/sections,craft://schema/fields,craft://schema/sections/{handle},craft://schema/fields/{handle} - Config resources:
craft://config/general,craft://config/routes,craft://config/sites,craft://config/volumes,craft://config/plugins - Content resources:
craft://entries/{section},craft://entries/{section}/{slug},craft://entries/{section}/stats
- Schema resources:
- 7 completion providers for intelligent parameter auto-completion
- Extension system for custom prompts via
EVENT_REGISTER_PROMPTSevent - Extension system for custom resources via
EVENT_REGISTER_RESOURCESevent - SSH tunnel configuration example for remote server access
- Comprehensive documentation for prompts (
docs/prompts.md) - Comprehensive documentation for resources (
docs/resources.md) - Extended documentation in
docs/extending.mdcovering prompts, resources, and completion providers - Hot-reload support for newly installed plugins without MCP server restart
Changed
- Tool count updated from 32 to 50 tools
- Documentation updated to reflect all available features
- Refactored error handling to align with MCP SDK conventions
- Applied Rector code quality improvements
- PSR-12 compliance across all files
- Added unit tests for new tool classes (BackupTools, CommerceTools, GraphqlTools, McpTools, SiteTools)
Version 1.0.1
January 3, 2026
Fixed
clear_cachestool now uses correctCraftFileHelper::clearDirectory()instead of non-existentFs::clearDirectory()read_logstool now dynamically finds all log files including date-based logs (e.g.,web-2026-01-03.log)read_logstool regex pattern updated to match Craft CMS log formatget_environmenttool removed non-existentgetTranslationsPath()call
Version 1.0.0
January 3, 2026
Added
- Initial release
- 32 MCP tools for Craft CMS
- Schema & Structure tools (list_plugins, list_sections, list_fields, list_volumes, list_routes, list_console_commands)
- Content tools (list_entries, get_entry, create_entry, update_entry, list_globals, list_categories, list_users)
- Asset tools (list_assets, get_asset, list_asset_folders)
- System tools (get_system_info, get_config, read_logs, get_last_error, clear_caches)
- Database tools (get_database_info, get_database_schema, get_table_counts, run_query)
- Tinker tool for PHP execution
- Debugging tools (get_queue_jobs, get_project_config_diff, get_deprecations, explain_query, get_environment, list_event_handlers)
- Extension system via
EVENT_REGISTER_TOOLSevent - Production safety defaults (disabled by default in production)
- Dangerous tools protection (tinker, run_query, create_entry, update_entry, clear_caches)