WordPress
Most of the sites an agency looks after run WordPress. One Application Password per website opens the same host several ways at once. One of them supplies a marketing source nobody else has: how often a client’s brand turns up, and gets linked, in AI answers. And under the same login an agent reads and edits that site’s pages, forms and media.
Where to find it
Section titled “Where to find it”The WordPress card on a website’s detail page (Domeinen & websites → Websites → the site). There is deliberately no menu item: WordPress is not a place you go, it is something a website has — the same reasoning that keeps Cloudflare on a domain rather than in the sidebar.
Switch the integration on under Instellingen → Integraties (Settings → Integrations). It needs the Websites module, because that is what the connection hangs off.
The credential, and what it actually is
Section titled “The credential, and what it actually is”- In WordPress, go to Users → Profile and scroll to Application Passwords. Create one and give it a name you will recognise later (“schakl”, say).
- In schakl., open the website and fill in the site address, the WordPress username and that password on the WordPress card.
- Press Controleren (Verify).
The password is stored encrypted, is write-only through the API, and never appears in a response, a log line or the activity trail. The trail records that it changed, never what to.
What Verify actually checks
Section titled “What Verify actually checks”Six independent probes run, and none of them gates another, because in reality those states are independent: Rank Math is routinely absent from a site whose posts API is perfectly healthy, and the MCP plugin from a site where Rank Math works fine.
| Capability | What a ✓ means | What a ✗ means |
|---|---|---|
rest | The password reaches the WordPress REST API | The credential itself was refused |
admin | That user is an administrator | Valid credential, but not an administrator |
abilities | WordPress 6.9’s Abilities API is present | WordPress is older than 6.9 |
rankmath_aiv | Rank Math answers on AI Visibility | Plugin absent, or no Content AI subscription |
mcp | The MCP Adapter plugin is installed | It is not |
bridge | The schakl WordPress MCP Bridge plugin is installed, with its version | It is not |
Two rules follow from that. A read that succeeds outranks a probe that refuses: a credential that reached the ordinary REST API is active even if the others failed, and only one refused by every probe is called refused. And unreachable and not WordPress are kept separate from refused, because neither says anything about the password — reporting them as an auth failure sends somebody off to re-mint a credential that was never wrong.
Every ✗ carries the site’s own error text, untranslated, because it is a quote: rest_no_route,
rest_forbidden, aiv_unauthorized. A ✗ with no explanation is the one state nobody can act on.
Four surfaces, one key
Section titled “Four surfaces, one key”Rank Math registers its AI Visibility features as WordPress Abilities, with two lines of metadata: show in REST, and expose over MCP. Those two lines are the whole story — they put the same functions on WordPress core’s REST route and on the MCP Adapter plugin’s MCP server, and both authenticate the way any REST caller does.
| Surface | What it is |
|---|---|
wp/v2 | The ordinary REST API: posts, media, users, plugins |
wp-abilities/v1 | WordPress 6.9’s Abilities API, opt-in per ability |
mcp/<server> | The MCP Adapter plugin’s MCP server |
rankmath/v1/ai-visibility | Rank Math’s own AI Visibility route |
AI Visibility as a marketing source
Section titled “AI Visibility as a marketing source”With the credential in place, link a marketing source of type Rank Math to this website under Marketing on the client. Without a connected website that link is refused at once, rather than turning up as a red line on tomorrow morning’s sync.
Five figures, and because none of them is a number a marketeer already owns, each one carries its own sentence on screen — written out where there is room, as a tooltip where there is not, in the same component for your team and for the client’s portal.
| Figure | What it counts | Scale |
|---|---|---|
| AI visibility score | Rank, mentions, citations and sentiment folded into one | 0–100 |
| Mentions | Times the brand was named in answers to the tracked prompts | Count |
| Citations | The subset of those that also linked to the brand’s site | Count |
| Average sentiment | How positively the brand is described | Percentage |
| Average rank | The brand’s mean place among the brands an answer names | 1 is best |
Why the trend line is ours and not Rank Math’s
Section titled “Why the trend line is ours and not Rank Math’s”Rank Math keeps no history: every upstream path answers “latest” only. So the nightly sync writes one snapshot per day and the trend line exists because we store those snapshots. Filling a thirty-day window with thirty identical rows would be a flat line that looks like measurement.
Analyses also run on the cadence of your Rank Math plan — weekly by default — so two consecutive snapshots may legitimately carry the same number. When it was last analysed travels with the figures, so a report can say what it is actually comparing rather than announcing a 0% week.
The site as an MCP surface
Section titled “The site as an MCP surface”The same login is how an agent reads and edits a client site’s content, from schakl.’s
MCP server, including on sites that register no abilities of their
own. A site is a parameter, never a tool: the /mcp/wordpress section is the same set of
tools whether your agency looks after four sites or four hundred. A tool per site would be a list
that differs per client, and a chat client puts every tool in the model’s context on every turn.
What differs per site, which plugins, which post types and which abilities, is answered by that
site’s summary.
| What | Permission |
|---|---|
| Which site is this client’s, and a summary: WordPress and PHP version, post types, plugin namespaces, forms, abilities, multilingual or not | wordpress.site.read |
| Read pages, posts and any custom post type, with drafts, ACF fields and the WPML language; look up media | wordpress.content.read |
| Create a draft or edit something that is not live yet | wordpress.content.write |
| Edit something a visitor sees, or set it live | wordpress.content.publish |
| Read / edit Contact Form 7: template, field names, both mails, messages | wordpress.forms.read / wordpress.forms.write |
| List abilities and run the ones that mark themselves read-only | wordpress.ability.read |
| Run any ability | wordpress.ability.run |
The audience decides the permission
Section titled “The audience decides the permission”WordPress has no staging: an edit to a published page is the broadcast. So the permissions are
not split by which field you touch but by who sees the result. A member writes drafts; anything
a visitor can see is wordpress.content.publish, admin only by default. That is decided by reading
the record before the write, and the refusal comes before the site is asked anything. Editing a
form sits with publish, because it is live the moment it saves and decides where a client’s leads
land. An ability that does not mark itself read-only is treated as a write: better to refuse
something harmless than to let something writing through.
Every write is a line in the site’s activity trail in schakl., naming the fields it touched. The client’s WordPress records nothing about us.
The passthrough to the whole REST API
Section titled “The passthrough to the whole REST API”Whatever has no fixed route goes through one bounded passthrough: any call the site’s REST API takes, under the stored password. It exists because real client sites run WPML, LiteSpeed, FileBird and Link Genius, and a route per plugin is a list that rots. It is also the route that hands an agent a WordPress administrator, so its bounds are the design:
wordpress.rest.readforGET(member),wordpress.rest.writefor anything else (admin only, never on a default key).- Writes to users, plugins, themes and settings are refused for everybody, before the site is asked. Those changes are made by a person in the site’s own admin. Reading the same routes is allowed.
- Paths are relative to
/wp-json/and cannot climb out of it. - Every write is a trail line, and every answer is capped at 256 KB with what was left out named.
schakl WordPress MCP Bridge: page builders, hidden post types, uploads, forms, menus and WPML
Section titled “schakl WordPress MCP Bridge: page builders, hidden post types, uploads, forms, menus and WPML”Many agency sites build a page out of an ACF repeater of typed blocks, each block type showing a handful of fields out of thirty. That cannot be edited through ACF’s REST integration, and none of it is fixable from the password’s side:
- ACF’s validation ignores conditional logic, so a hidden required field blocks every write;
- a repeater has to go back whole and saves by position, so changing one block means resending every block;
- a post type not shown in REST does not exist there;
wp/v2/mediaonly takes multipart, which no MCP tool can send.
So a WordPress plugin does it on the site itself: the schakl WordPress MCP Bridge (its own
repository, schakl-wordpress-mcp-bridge, PHP 7.4+, GPL). It knows the field groups’ schema including when the editor hides a field, reads per
block only the fields the editor shows, writes with conditional logic evaluated the way the
editor does, reports every problem with its full path (blokken[2].titel) instead of stopping at
the first, edits one block without touching the others, takes an image as a URL or base64, and
handles WPML: languages, a linked translation with every reference swapped for its
translation, and String Translation.
schakl. reaches the plugin through 33 bridge routes under the same login, and so through the
same 33 tools in /mcp/wordpress however many sites there are:
| What | Permission |
|---|---|
| Versions, post types (including ones not shown in REST), taxonomies, options pages, menus, languages | wordpress.site.read |
| The field schema; read records, terms, options pages and menus | wordpress.content.read |
| Create or edit records and terms, upload media, create or connect a translation | wordpress.content.write (+ publish for what is live) |
| Change ACF options pages and translated strings: live the moment they save | wordpress.content.publish |
| Create a menu, rename it or change which theme locations it fills; add, edit, move and remove items, and put them in order | wordpress.content.publish |
| Trash or delete a record; delete a menu with all its items | wordpress.content.delete |
| Read Contact Form 7 forms with the fields parsed from the template and CF7’s own configuration errors | wordpress.forms.read |
| Create or edit a form, or make its translation | wordpress.forms.write |
| Delete a form | wordpress.forms.delete |
A record, a term or an options page can also be read as text rather than as a field tree: the title, the body without markup and the ACF fields in order, with choices by their label. The record list takes the same option, so an agent pulls every FAQ item with its answer in one call, at a fraction of the size.
Menus are edited in place: an item keeps whatever you do not send, can be pointed at another page or term, moved under another parent and positioned among its siblings, and a custom link’s URL can be changed. Deleting a whole menu is permanent and sits under the delete permission, like deleting a record.
Contact Form 7 is reachable two ways. The plain form routes above go through CF7’s own REST namespace and keep working on a site without the plugin. The bridge adds what that namespace never had: the fields CF7 parses off the template, its configuration validator’s objections (CF7 sends nothing while a mail is misconfigured), mails and messages merged over what the form holds so a changed recipient does not blank the subject, a delete, and WPML. A form deletes permanently (CF7 has no trash) and empties every page that embeds it, hence its own permission.
WPML knows a form two ways, and both are served. Where forms are a translatable post type, each language gets a linked copy of its own, and the answer carries the shortcode a page in that language embeds. Where the Contact Form 7 Multilingual add-on serves every language from one form, its texts are String Translation strings, read and written on the form itself.
Where the plugin is not installed the call says so itself, naming the plugin; on the WordPress card the verify shows which version is there, and under it whether the plugin can update itself: it updates itself (automatically, where WordPress is set to), a newer release is available, there is no GitHub token, or the update check failed. A site without a working token links to the plugin’s settings screen. A plugin older than 1.3.1 does not report this, and the card says not reported, never cannot update. A refusal from the plugin comes through unchanged, with the path to the field and the row, because a translated sentence without the row index sends an agent hunting.
Permissions
Section titled “Permissions”| Permission | What it opens | Default |
|---|---|---|
wordpress.site.manage | Connect, change the password, verify, disconnect | Administrator |
wordpress.site.read | See whether this client’s site is connected, what the last verify found, and the summary | Administrator, Member |
wordpress.content.read | Read pages, posts, media and bridge records | Administrator, Member |
wordpress.content.write | Write drafts, edit what is not live, upload media | Administrator, Member |
wordpress.content.publish | Edit anything a visitor sees, set something live, options pages, menus and strings | Administrator |
wordpress.content.delete | Trash or delete records, delete menus | Administrator |
wordpress.forms.read / wordpress.forms.write | Read / edit Contact Form 7 | Member / Administrator |
wordpress.forms.delete | Delete Contact Form 7 forms through the bridge | Administrator |
wordpress.ability.read / wordpress.ability.run | Read-only abilities / any ability | Member / Administrator |
wordpress.rest.read / wordpress.rest.write | The REST passthrough: read / write | Member / Administrator |
The read goes to members because “is this client’s site connected?” is a question an account
manager asks while doing ordinary work. The manage permission stays admin-only and is deliberately
not folded into websites.website.write.
Good to know
Section titled “Good to know”- Disconnecting forgets the password here; it does not revoke it at WordPress. That is the owner’s act on their own profile screen, and doing it as a side effect of tidying a list would break whatever else that password was minted for.
- The card renders instantly, connected or not. It reads stored state and touches no outside service, so a website page loads at the same speed whether the client’s WordPress is up, down, behind a firewall or gone. Going and looking is what the Verify button is for.
- A verify that succeeds clears the error a previous one set. A status flag that only ever turns on is a bug with a long tail.
- Rank Math is read only. No creating a brand and no editing prompts. Writing to the site itself goes through the MCP routes above, under their own permissions.
- Restricted colleagues see only their own clients’ sites. A website belongs to its domain’s client, which is two joins away — and it is enforced, not assumed.