High risk. Opt-in. This skill documents MCP tools that can permanently publish PHP on the WordPress site. Install and invoke it only when the user explicitly wants snippet work.
For templates, pages, i18n, media, settings, and Tailwind — use canai-mcp instead. Do not blend YOLO workflows into a normal /canai-mcp content session unless the user asked for this skill.
Transport is the same CanAI MCP server ({site}/wp-json/mcp/wpcanai) and API key as canai-mcp. Ability IDs use slashes; MCP tool names use hyphens.
CRITICAL — when to use this skill
Use canai-yolo only for:
- FluentSnippets (
easy-code-manager) — list/read/create/update/publish site PHP, CSS, or JS snippets via dedicated MCP tools.
There is no eval escape hatch. The
wpcanai/evalability was removed in plugin v1.59.0 — WP.org banseval()outright — andWPCANAI_ENABLE_EVALis no longer read. Every capability is a real ability; there is nothing to fall back on. If a workflow seems to need eval, the fix is to request the missing ability, not to improvise. An opt-in add-on plugin is planned (specs/2026-08-20-canai-eval-addon-design.md).
FluentSnippets (PHP/CSS/JS snippets)
Site-specific PHP, CSS and JS often lives in FluentSnippets rather than plugin code. Tools: wpcanai-list-snippets, wpcanai-get-snippet, wpcanai-create-snippet, wpcanai-update-snippet, wpcanai-replace-in-snippet, wpcanai-set-snippet-status. Their descriptions start with [fluent-snippets] / [fluent-snippets, filesystem] (plus cache when the site purges its page cache after writes) — see Tool labels in canai-mcp.
Key rules the schemas alone don't make obvious:
-
Tools only appear when FluentSnippets is active. If
easy-code-manageris inactive the six snippet tools are not registered at all — you won't see them in the tool list. -
Canonical form (plugin v1.80.0). Send PHP as the body without
<?php— a leading<?phpand blank lines are stripped on write (no longer rejected) and on read. Trailing newlines are kept; a trailing?>is removed.sha256/bytes/linesare computed over the code exactly asget-snippetreturns it, so hash that string, not your local file, when comparing. css/js must not be wrapped in their own<style>/<script>tag. PHP is syntax-checked and test-run once before it is saved: it must not redeclare a function or class that already exists (guard withfunction_exists/class_exists), print output,returnat the top level, or call something that only loads later (use a hook). -
Create defaults to a draft.
wpcanai-create-snippetwrites a draft unless you passstatus: "published"(v1.80.0), which syntax-checks PHP first and writes nothing on a syntax error. The responsestatusis read back — Fluent Snippets' ownauto_publishcan still promote a draft. -
Writes only succeed for allowlisted groups. An administrator lists agent-writable snippet groups at CanAI → AI Client → Guardrails → FluentSnippets group allowlist. An empty allowlist (the default) means all snippet writes are refused (
snippet_writes_disabled). A write to a non-allowlisted group returnssnippet_group_not_allowed. Prefer the groupAIunless the user specifies otherwise. Reads (list/get) are always allowed regardless of the allowlist. -
Updates are sparse. Send only the fields you want to change to
wpcanai-update-snippet; the server reads the current snippet, merges your changes over its full metadata, and writes it back. Omitting a field keeps its current value — you cannot wipegroup/priority/run_at/created_atby sending onlycode. -
run_atis type-specific. PHP →all|backend|frontend;php_content→shortcode|wp_head|wp_body_open|wp_footer|before_content|after_content; css →wp_head|admin_head|everywhere; js →wp_head|wp_footer|admin_head|admin_footer. An invalid pairing is rejected before FluentSnippets is called (invalid_snippet_type/invalid_run_at). -
Upstream quirks (not fixed here). CSS
everywheredoes not actually load in admin (a plugin typo,everywehere), and PHPfrontendis not enforced (it runs everywhere) — both values are still accepted because they are what the FluentSnippets UI offers. Preferall/backend/wp_headwhen unsure. -
Reactivation. If a snippet fatally errored, FluentSnippets auto-disables it and
has_erroris true. An update to it is refused (snippet_has_error) unless you pass"reactivate": true, which clears the error and applies the update — so a routine edit can't silently re-arm known-broken code. -
Errors explain themselves (v1.80.0). A save Fluent Snippets refuses comes back as
"{title} on line {line}: {reason} Fix: {fix}"in the error message — read the reason and the fix before retrying; don't resend the same code.
Moving a snippet to a different group (via wpcanai-update-snippet's group) requires both the current group and the destination group to be allowlisted.
Deploy loop (plugin v1.80.0)
Edit a live snippet without resending it and confirm it without a browser:
- Replace —
wpcanai-replace-in-snippet { "file_name", "replacements": [...], "require_all": true, "expected_sha256"?: "<sha256 from your last read>" }.require_allmakes a miss write nothing and name the nearest match;expected_sha256refuses the write withhash_mismatch(409) if someone changed the snippet since you read it. Usedry_run: truefirst when unsure of the anchors. Whole-body rewrites still go throughwpcanai-update-snippet(sameexpected_sha256). - Check — the response carries
sha256,bytesandlinesof the code as stored, read back after the write. Comparesha256with the hash of the code you expect (over the canonical form above). Later, or from another session,wpcanai-get-snippet { "file_name", "fields": ["hash"] }returns the same fields without the code. - Cache — the response's
purgedsays whether a page-cache purge is pending:{ "scope": "all", "plugin": "…" }means the whole site is purged at the end of the request (a snippet runs on every page);nullmeans nothing was queued. Callwpcanai-purge-cache(incanai-mcp) only whenpurgedisnullwhile a cache plugin is active (purge after writes is off), or for a URL outside the write.
Writing custom REST routes in a snippet
Agents often implement browser → WordPress relays as a FluentSnippets PHP route plus _canai_js fetch. Two rules the schemas alone don't make obvious:
-
Never send a custom nonce under
X-WP-Nonce(or request param_wpnonce). WordPress core'srest_cookie_check_errors()intercepts that exact header/param on every REST request site-wide and validates it against the built-in'wp_rest'action — before your route'spermission_callbackruns. A custom-action nonce under that name is always rejected withrest_cookie_invalid_nonce, masking your own permission logic. Use any other header name for a custom nonce action (e.g.X-<Your-App>-Nonce). -
Surface HTTP status / error code to the client (console log or distinct on-page states) even when user-facing copy stays generic. A rate-limit
429, a nonce403, and a real500are different failures; collapsing them into one message makes production incidents impossible to triage.
wpcanai-list-snippets
- Args:
{ "status"?: string, "type"?: string, "group"?: string, "include_hash"?: bool }— all optional filters (status:published|draft;type:PHP|php_content|css|js;group: group name). (v1.80.0)include_hash: trueaddssha256,bytes,linesper snippet (reads every file, so opt-in). Only appears when FluentSnippets is active. - Returns:
{ "snippets": [{ "file_name", "name", "type", "status", "run_at", "priority", "group", "tags", "description", "created_at", "updated_at", "has_error", "error_message", "writable" }], "groups": string[], "writable_groups": string[] }—writableis per-snippet (its group is on the allowlist);writable_groupsis the current allowlist so you know what you may touch before attempting a write. Reads are unrestricted.
wpcanai-get-snippet
- Args:
{ "file_name": string, "fields"?: ["meta"|"code"|"hash"] }— the snippet's file name (its identifier, e.g.3-my-snippet.php);fieldsdefaults to all three. - Returns: full metadata + the
codebody + error state +writable(same fields as alist-snippetsrow pluscodeandcondition) + (v1.80.0)sha256,bytes,lines.fields: ["hash"]returns onlyfile_name,status,group,updated_at,sha256,bytes,lines— use it to confirm a write without pulling the code. Errorsnippet_not_foundfor an unknown file.
wpcanai-create-snippet
- Args:
{ "name": string, "type": string, "run_at": string, "group": string, "code": string, "description"?: string, "tags"?: string, "priority"?: int, "status"?: "draft"|"published" }— all ofname/type/run_at/group/coderequired. PHPcodegoes without<?php(a leading one is stripped).type/run_atare validated against the type table.groupmust be on the writable allowlist.statusdefaults todraft;publishedlints PHP first. - Returns:
{ "success": true, "file_name": string, "status": string, "group": string, "note": string, "sha256", "bytes", "lines", "purged" }— the file name is auto-generated (you can't choose it) and is what every later call keys on.statusis read back; a draft goes live later withwpcanai-set-snippet-status.
wpcanai-update-snippet
- Args:
{ "file_name": string, "code"?: string, "name"?: string, "description"?: string, "tags"?: string, "group"?: string, "run_at"?: string, "priority"?: int, "reactivate"?: bool, "expected_sha256"?: string }—file_namerequired; send only fields to change (sparse — the server merges over current metadata). The current group (and destinationgroup, if moving) must be allowlisted. An errored snippet needsreactivate: true. (v1.80.0)expected_sha256refuses a stale write withhash_mismatch(409, both hashes in the message, nothing written). - Returns:
{ "success": true, "file_name": string, "changed_fields": string[], "sha256", "bytes", "lines", "purged" }— hash fields read back after the write. - Published PHP code updates: Updating
codeon a published PHP snippet used to fatal withCannot redeclare function(FluentSnippets validates in the same request where the live snippet is already loaded). CanAI handles this transparently. If an older plugin build still returns that error, unpublish → update → republish viawpcanai-set-snippet-status.
wpcanai-replace-in-snippet (plugin v1.80.0)
- Args:
{ "file_name": string, "replacements": [{ "from": string, "to": string }], "require_all"?: bool, "ignore_whitespace"?: bool, "regex"?: bool, "scope"?: { "text": string, "occurrence"?: int }, "dry_run"?: bool, "expected_sha256"?: string }— thewpcanai-replace-in-metacontract (seecanai-mcp) applied to a snippet's code: samerequire_allmiss diagnostics in thereplace_no_matchmessage, sameignore_whitespace/regex/scope/dry_run. The write goes throughupdate-snippet(published PHP is linted first) and the same group allowlist. - Returns:
{ "success", "file_name", "total", "dry_run", "replacements": [{ "from", "to", "count", "suggestion"? }], "sha256", "bytes", "lines", "purged"? }— ondry_runthe hash is of the current code and there is nopurged.
wpcanai-set-snippet-status
- Args:
{ "file_name": string, "status": "published"|"draft" }— the snippet's group must be allowlisted. - Returns:
{ "success": true, "file_name": string, "status": string, "sha256", "bytes", "lines", "purged" }. Fires both FluentSnippets lifecycle actions so the index cache rebuilds and a published snippet actually runs.
Action router (quick)
| Goal | Tools |
|---|---|
| List / read FluentSnippets snippets | wpcanai-list-snippets / wpcanai-get-snippet |
| Create / update a snippet | wpcanai-create-snippet (status optional) / wpcanai-update-snippet |
| Edit part of a snippet (deploy loop) | wpcanai-replace-in-snippet → check sha256 → purged |
| Confirm what is stored, no code | wpcanai-get-snippet { "fields": ["hash"] } / wpcanai-list-snippets { "include_hash": true } |
| Publish / unpublish a snippet | wpcanai-set-snippet-status |
Install
npx skills add usewp/canai --skill canai-yolo
Pair with canai-mcp for content work on the same MCP endpoint.