The boundary is a Craft user — not a promise.
An AI client that can change a live site is only as safe as the limits around it. Here is every limit this plugin puts in the way, in the order a call meets them, and what each one does not cover.
What every call passes through
In this order. A call that fails any step never reaches the tool.
- Rate limit — calls per minute, counted per token. A brake against a looping model, not a defence against a determined caller who holds a valid token.
- Token scope —
read,contentorfull. The scope only narrows what the mapped user may already do. - Edition — the plugin ships as a single edition, so this step always passes today. It exists so that splitting editions later cannot quietly hand out a tool.
- Read-only mode — while on, nothing that changes state runs, whatever the settings below say.
- Tool enablement — every tool that changes state is off until an administrator switches it on, one at a time.
- Administrator check — the administrator tools additionally require the mapped user to be an administrator.
- Argument schema — arguments are validated against the tool's schema before the tool sees them.
- Craft's own permission check, per element — then the tool asks Craft, for this element and this site, whether the mapped user may view, save or delete it.
Two kinds of refusal, on purpose. A tool that does not exist and a tool that the token's scope does not cover are answered identically — Unknown tool — because the difference would tell an attacker what else is there. A tool that is switched off, blocked by read-only mode or reserved for administrators is not listed either, but a direct call gets the reason in plain words, so an operator does not go looking for the fault on their own side.
Why the user is the real boundary
Every token belongs to exactly one Craft user. Two questions are asked, and the second is the one that matters: first the coarse one, whether this kind of action is open to this user at all, and then the precise one — may this user do this, to this element, on this site. An editor who may only work on one site and see one section reaches exactly that through the AI client, and nothing else.
This has a consequence worth stating plainly: a token that belongs to an administrator can do what an administrator can do. Give a token the smallest user that still gets the job done, and use the scope to narrow it further.
An unknown or forbidden section or category group yields an empty list, not an error — an error would confirm that the thing exists. A site handle that does not exist is reported as an error instead, because a wrong handle is a mistake in the call, not a boundary.
Tokens
- The plain text is shown exactly once, when the token is created. Only a SHA-256 hash is stored, so it cannot be shown again.
- Tokens live in their own table, never in the project config — which is committed to Git.
- The token page in the control panel is open to administrators only. Craft registers a permission "Access AlphaBridge MCP" for every plugin with a sidebar entry; granting it does not open this page to non-administrators.
- A token can carry an expiry date, and can be revoked at any time; the next call with it fails.
- The
Authorizationheader is redacted in Craft's own request-context log entries, so a token does not end up instorage/logswhen a request is logged.
The audit log, and what it deliberately omits
Every tool call is recorded: when, which tool, which Craft user, and the outcome — allowed, refused or failed. Refused calls are recorded too; they are the interesting ones.
Argument names are recorded, argument values are not. You can see that an entry was created with a section and a title; you cannot read the title out of the log. That is deliberate: a log that stored values would be a second, less protected copy of your content.
Reading the log needs the administrator scope. The log is trimmed to a fixed size, so it cannot grow without bound.
One honest limit: writing the log entry is best effort. If that write itself fails — a database error, say — the call is not aborted; the failure goes to Craft's own log instead. So treat the audit log as a very good record, not as a guarantee that nothing can be missing.
Reading Craft's logs is narrower than it looks
The log tool returns Craft's log entries without the request context that Craft attaches to them. That context contains $_POST, $_COOKIE and $_SESSION — including, on a failed login, a password in clear text. SQL statements and stack traces are cut off at the first marker.
This is why the log tool is an administrator tool and why it is off by default.
The database tools, and their limits
Two tools reach the database, and only one of them runs SQL that you write. craft_db_schema lists table names and marks the protected ones as unreadable. craft_db_query runs one SELECT against one table and rejects, before anything reaches the database:
- joins, subqueries, common table expressions and
UNION - every function except
count,min,max,avgandsum - protected tables — users, sessions, tokens, the project config and others that hold credentials or secrets
- expressions that name the server account or a sequence
A barrier made of rules only knows what it was told. That is why both are administrator tools, why the query tool is off by default, and why neither ever writes.
Origins and DNS rebinding
If a request carries an Origin header, it must be on the allowlist or the endpoint answers 403. The allowlist is what you configured plus the site URL, but only where that can be determined statically. A site URL built from Craft's @web alias does not count: Craft derives @web from the Host header of the incoming request, so an attacker would be compared against a value they chose themselves.
In such an installation the configured list is the only source, and if it is empty every request that carries an Origin is refused. Server-to-server clients send no Origin and are unaffected.
What this plugin does not do
Each of these is a decision, and each removes a class of risk rather than managing it.
- No arbitrary code execution. No tool runs PHP, shell commands or a template you pass in.
- No deployment and no file transfer. The plugin does not move files and never touches a server over SSH or FTP.
- No plugin lifecycle. Nothing installs, enables, disables or removes plugins.
- No writes to project config or to users. Craft enforces the rules for those in its controllers; this plugin talks to the element API, which does not see those rules — so it does not write there at all.
- No tags. Craft defines no permission for tags, so any boundary would have to be invented rather than enforced.
- No field-setting values. Field settings come back as names only, never as values, because a plugin can put anything into them. This is not a blanket promise about secrets: the database tool reaches every physical table whose name is not on its protected list, and a third-party table with an inconspicuous name is not on it. That tool is for administrators and off by default for exactly this reason.
How this is kept true
Every change runs a unit suite, static analysis at PHPStan level 6 without a baseline, and an end-to-end run against a freshly built Craft installation on PostgreSQL 16 and MySQL 8.4. That run creates a non-administrator editor who may see exactly one section and work on exactly one site, and proves that the permission filter really cuts — not merely that the tools return something.
Found something? Please report it privately to mail@cultureclub.dev, not in a public issue.