
Er zijn drie manieren om een agent aan WordPress te koppelen: de Abilities API van de kern (slechts drie abilities), de officiële adapter WordPress/mcp-adapter (v0.6.1 van 13 augustus 2026), en een eigen eindpunt. De derde is de enige waarbij jij alles beslist: 152 regels PHP, drie leestools, een eigen token en een SQL-filter dat elke schrijfactie omzet in SELECT 1. Hier getest met curl en vanuit Codex, tegen een lokale WordPress 7.1.
Een code-agent aan WordPress koppelen betekent dat je hem toegang geeft tot je artikelen, je
concepten en, al snel, het recht om te schrijven. Tussen het beheerderswachtwoord en de botte
weigering bestaat een houdbare positie: een MCP-eindpunt dat je zelf schrijft, dat maar drie
lezingen blootstelt en dat niet in staat is in de database te schrijven, zelfs als het model erom
vraagt.
Dit artikel blijft dicht bij WordPress zelf. Voor het protocol op zich, JSON-RPC, transport, de
initialize-cyclus, lees
een MCP-server bouwen in PHP,
hier gaat het over wp-load.php, $wpdb en applicatiewachtwoorden.
Wat WordPress in september 2026 biedt
Er bestaan drie wegen, en ze sluiten elkaar niet uit. De blogpost
From
Abilities to AI agents, gepubliceerd op 4 februari 2026 op developer.wordpress.org, zet de leer
uiteen. Dit is hun status op 7 september 2026, repository per repository gecontroleerd.
| Weg | Status | Wat ze vereist |
|---|---|---|
| Abilities API van de kern | In WordPress sinds 6.9, drie abilities geregistreerd op mijn WordPress 7.1 | Niets te installeren, maar er wordt niets vanzelf als MCP blootgesteld |
Officiële MCP-adapter (WordPress/mcp-adapter) | Actief: v0.6.1 van 13 augustus 2026, laatste commit op 2 september 2026, 1.672 sterren | Een plugin of een composer require, plus een applicatiewachtwoord of OAuth 2.1 |
Plugin Automattic/wordpress-mcp | Gearchiveerd: laatste commit op 30 augustus 2025, repository alleen-lezend sinds 19 januari 2026 | Niets meer: hij verwijst naar de officiële adapter |
De omslag heeft nog niet overal plaatsgevonden: de
tutorial van LWS,
gepubliceerd op 2 september 2025, beschrijft de plugin van Automattic nog altijd als de hoofdweg,
terwijl WPFormation,
bijgewerkt op 13 augustus 2026, is overgestapt op de officiële adapter. De GitHub-API geeft de
doorslag: archived: true aan de ene kant, archived: false aan de andere.
Wat de Abilities API van jouw WordPress bevat
De Abilities API is de basis: een register van gedeclareerde capaciteiten, met een invoerschema,
een uitvoerschema, een categorie en een permissiecontrole. De MCP-adapter doet niets anders dan dat
register vertalen naar MCP-tools. Met andere woorden: zonder ability stelt een MCP-adapter niets
bloot.
Op de WordPress 7.1 van mijn lokale omgeving registreert de kern er drie. De eerste verrassing
is dat de REST-route niet open staat:
curl -s http://localhost:8090/wp-json/wp-abilities/v1/abilities
# {"code":"rest_forbidden","message":"Sorry, dat mag je niet doen.",
# "data":{"status":401}}Als beheerder, via WP-CLI en rest_do_request() om te vermijden dat ik ook maar één
identifier moet aanmaken, past de lijst in drie regels:
docker compose run --rm -T cli wp eval '
$u = get_users( array( "role" => "administrator", "number" => 1 ) );
wp_set_current_user( $u[0]->ID );
$r = rest_do_request( new WP_REST_Request( "GET", "/wp-abilities/v1/abilities" ) );
foreach ( $r->get_data() as $a ) { echo $a["name"], "\n"; }'
# core/get-site-info
# core/get-user-info
# core/get-environment-infoTwee details tellen voor het vervolg. Elke ability draagt om te beginnen annotaties, en die zijn bindend. core/get-site-info declareert meta.annotations.readonly = true, en de uitvoerroute weigert dan de POST:
# POST op een alleen-lezen ability
# HTTP 405: {"code":"rest_ability_invalid_method",
# "message":"Alleen-lezen abilities vereisen de GET-methode."}
# GET op dezelfde route, als beheerder: HTTP 200
# {"name":"Gekkode","url":"http://localhost:8090","admin_email":"c***@gekkode.com",
# "charset":"UTF-8","language":"fr-FR","version":"7.1"}Het tweede detail lees je in het antwoord zelf, admin_email. De onschuldigste ability van de kern, degene die gemarkeerd staat als “alleen-lezend, niet-destructief, idempotent”, geeft het administratie-e-mailadres van de site terug. Een agent die haar aanroept, zet dat in zijn context, en die context vertrekt naar een modelleverancier. Dat is precies het soort lek dat geen enkel alarm afgaat: er is geen enkele data gewijzigd, alles is verlopen zoals voorzien.
Dezelfde aanroep als anonieme bezoeker geeft 401 rest_ability_cannot_execute terug: de WordPress-permissies worden wel degelijk gerespecteerd. Het permissiemodel doet zijn werk. Het probleem komt van het account dat je de agent geeft, met het recht om alles te lezen.
Waarom je eigen eindpunt schrijven?
Omdat het standaardoppervlak enorm is. Op deze site declareert de index van de REST-API
154 routes voor 296.774 bytes aan schema. Een agent die dat
oppervlak verkent, betaalt daarvoor in tokens, en niets weerhoudt hem ervan de route te vinden die
schrijft.
De meest sprekende vergelijking gaat over een identieke zoekopdracht:
| Aanroep | Grootte van het antwoord |
|---|---|
GET /wp/v2/posts?search=docker&per_page=5 | 116.227 bytes |
Dezelfde met _fields=id,title,link,date | 1.080 bytes |
search_posts("docker", 5), mijn MCP-tool | 1.499 bytes |
Lees de derde regel goed: mijn tool is groter dan de goed geparametriseerde REST-API,
omdat hij ook nog het aantal woorden telt. De winst komt dus niet van MCP, maar van het feit dat de
vorm van het antwoord één keer wordt beslist, door jou, in plaats van aan het model te worden
overgelaten, dat geen enkele reden heeft om aan _fields te denken. De factor 78 van de
ruwe aanroepen, dat is wat jij vastlegt door de tool te schrijven.
Het alleen-lezen eindpunt schrijven
Het bestand telt 152 regels effectieve code. Het laadt wp-load.php, stelt drie
tools bloot en plaatst twee sloten. Het eerste is een eigen bearer-token, gelezen uit de omgeving:
dat is geen WordPress-wachtwoord, geen applicatiewachtwoord, en het geeft nergens anders toegang
toe dan tot deze drie lezingen.
$attendu = (string) getenv( 'GK_MCP_TOKEN' );
// Onder Apache/mod_php is $_SERVER['HTTP_AUTHORIZATION'] leeg: de header komt alleen
// binnen via getallheaders(). Gemeten op wordpress:7.1-php8.5-apache.
$entetes = function_exists( 'getallheaders' ) ? array_change_key_case( getallheaders() ) : array();
$recu = (string) ( $_SERVER['HTTP_AUTHORIZATION']
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
?? $entetes['authorization']
?? '' );
$recu = preg_replace( '/^Bearer\s+/i', '', trim( $recu ) );
if ( '' === $attendu || ! hash_equals( $attendu, $recu ) ) {
header( 'Content-Type: application/json', true, 401 );
header( 'WWW-Authenticate: Bearer realm="mcp"' );
echo json_encode( array( 'error' => 'jeton absent ou invalide' ) );
exit;
}Het tweede slot is de kern van de opzet. De WordPress-rol beperken is niet genoeg: elke
extensie die tijdens het opstarten wordt geladen, kan schrijven. Dus snijden we lager af, op
SQL-niveau. WordPress stuurt elke query door het filter query
(wp-includes/class-wpdb.php), en het weet filters te laden die vóór hem zijn
gedeclareerd: wp-includes/plugin.php roept
WP_Hook::build_preinitialized_hooks( $wp_filter ) aan op regel 41. Daar nestelen we
ons.
$GLOBALS['gk_bloquees'] = array();
function gk_lecture_seule( $sql ) {
if ( preg_match( '/^\s*(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN|SET|USE)\b/i', (string) $sql ) ) {
return $sql;
}
$GLOBALS['gk_bloquees'][] = substr( preg_replace( '/\s+/', ' ', (string) $sql ), 0, 120 );
return 'SELECT 1 /* écriture refusée par le point d\'entrée MCP */';
}
// Gelezen door WP_Hook::build_preinitialized_hooks() bij het laden van wp-includes/plugin.php.
$wp_filter = array(
'query' => array( 0 => array( array( 'function' => 'gk_lecture_seule', 'accepted_args' => 1 ) ) ),
);
define( 'DISABLE_WP_CRON', true );
require_once '/var/www/html/wp-load.php';Elke query die geen lezing is, wordt vervangen door een SELECT 1 en gelogd. Ik heb
het slot gecontroleerd met een moedwillige DELETE, geschreven om met geen enkele rij
overeen te komen: als het filter zou falen, zou er niets vernietigd worden.
docker exec gekkode-mcp-lab php .../test-verrou.php
# Schrijfpogingen tijdens het opstarten van WordPress: 0
# Na een moedwillige DELETE, werkelijk verzonden query: SELECT 1 /* écriture refusée */
# Totaal geblokkeerde schrijfacties: 1
# - DELETE FROM gk_options WHERE option_id = 0Nul schrijfacties bij het opstarten: op deze site probeert WordPress niets in de database
tijdens een gewone laadbeurt. Dat is goed nieuws, geen garantie: voeg een extensie toe en de
telling verandert. Het slot dient voor die dag.
Dan de drie tools. Ze zijn bewust karig: search_posts ziet alleen gepubliceerde
artikelen, get_post weigert alles wat geen gepubliceerd artikel is, en
site_stats telt. Elke tool is geannoteerd met readOnlyHint, verderop zie
je dat dat niet decoratief is.
case 'search_posts':
$q = new WP_Query( array(
'post_type' => 'post',
'post_status' => 'publish', // nooit de concepten
's' => (string) ( $args['query'] ?? '' ),
'posts_per_page' => min( 20, max( 1, (int) ( $args['per_page'] ?? 5 ) ) ),
'no_found_rows' => true,
) );
// vervolgens, per artikel: ID, titel, permalink, datum, aantal woorden.Het toegangspunt starten en met curl controleren
De ingebouwde HTTP-server van PHP volstaat, en heeft een voordeel: het eindpunt luistert op een
andere poort dan die van de site, alleen op de lokale loopback. Niets wordt publiek blootgesteld.
Bij mij draait het geheel in een wegwerpcontainer die aansluit op het Docker-netwerk van de blog.
# Het token wordt nooit in een bestand van de repository geschreven.
export GK_MCP_TOKEN=$(openssl rand -hex 16)
docker run -d --name gekkode-mcp-lab --network gekkode-blog_default \
-p 127.0.0.1:8099:8099 -e GK_MCP_TOKEN="$GK_MCP_TOKEN" \
-e WORDPRESS_DB_HOST=db -e WORDPRESS_DB_NAME=gekkode \
-e WORDPRESS_DB_USER=gekkode -e WORDPRESS_DB_PASSWORD=gekkode \
-v "$PWD":/var/www/html wordpress:7.1-php8.5-apache \
php -S 0.0.0.0:8099 /var/www/html/chemin/vers/mcp-wp.phpDe eerste test is die van de weigering.
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8099/mcp
# 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:8099/mcp \
-H 'Authorization: Bearer faux'
# 401Dan de handshake, zoals een MCP-client die zou doen:
curl -s -X POST http://127.0.0.1:8099/mcp \
-H "Authorization: Bearer $GK_MCP_TOKEN" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18","capabilities":{}}}'
# {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18",
# "capabilities":{"tools":{}},"serverInfo":{"name":"gekkode-wp-lecture","version":"1.0.0"}}}curl -s -X POST http://127.0.0.1:8099/mcp \
-H "Authorization: Bearer $GK_MCP_TOKEN" -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"site_stats","arguments":{}}}'
# {"wordpress":"7.1","site":"http://localhost:8090",
# "articles":{"publies":1198,"brouillons":12,"futurs":0},
# "pages":56,"categories":11,"etiquettes":64}De responstijden, gemeten op deze lokale spiegel van 1.198 artikelen: 0,11 s voor
tools/list, 0,02 s voor site_stats, 0,07 s voor
search_posts, 0,03 s voor get_post. Het gros zit in het opstarten van
WordPress, niet in de query.
En de controle die telt: om een concept vragen.
{"jsonrpc":"2.0","id":6,"result":{"isError":true,
"content":[{"type":"text","text":"article introuvable ou non publié"}]}}Het aan Codex koppelen
Codex bewaart zijn MCP-servers in ~/.codex/config.toml. Met de optie -c hoef je dat bestand niet aan te raken: de hele declaratie gaat op de commandoregel mee, wat ik hier heb gedaan, met
codex-cli 0.153.4 en GPT-6 Astra.
export GK_MCP_TOKEN=…
codex exec \
-c 'mcp_servers.wp.url="http://127.0.0.1:8099/mcp"' \
-c 'mcp_servers.wp.bearer_token_env_var="GK_MCP_TOKEN"' \
-m gpt-6-astra \
"Utilise UNIQUEMENT les outils MCP du serveur wp. Appelle site_stats, puis search_posts
avec la requête « websocket » et per_page 2."Eerste poging: een leerzame mislukking. De server verbindt, de tools worden ontdekt, maar de
aanroepen stoppen abrupt.
mcp: wp/site_stats started
mcp: wp/site_stats (failed)
MCP tool call requires approval, but approval policy is neverIn een interactieve sessie zou Codex je vragen elke aanroep goed te keuren. In
codex exec kan niemand antwoorden: het beleid staat op never, de aanroep
wordt geweigerd. De instelling wordt per server bepaald:
[mcp_servers.wp]
url = "http://127.0.0.1:8099/mcp"
bearer_token_env_var = "GK_MCP_TOKEN"
default_tools_approval_mode = "writes" # auto | prompt | writes | approve
startup_timeout_sec = 20writes is het juiste compromis: alleen de tools die zichzelf als niet-wijzigend
aankondigen, komen door zonder te vragen. Met deze instelling en de annotaties op hun plaats,
slaagt de uitvoering in twintig seconden voor 9.550 tokens:
mcp: wp/site_stats (completed)
mcp: wp/search_posts (completed)
Articles publiés : 1 198
Version de WordPress : 7.1
Titres trouvés : « Créer un serveur WebSocket en PHP » ; « Comment faire une requête cURL en PHP »Ik wilde precies weten wat deze toestemming veroorzaakte. Dus heb ik hetzelfde bestand opnieuw gebruikt, ontdaan van zijn annotaties, zonder verder iets te veranderen: dezelfde aanroep werd weer MCP tool call requires approval. De trigger is dus niet de naam van de tool en evenmin zijn beschrijving, maar de vlag die de server zelf declareert.
'annotations' => array(
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
),Onthoud de richting van de afhankelijkheid: het is jouw server die beweert onschadelijk te zijn,
en de client die dat gelooft. Een MCP-server van een derde partij kan liegen over deze regel. De
redenering geldt dus voor code die je zelf hebt geschreven, niet voor een server die je van een
marketplace hebt gehaald, een onderwerp dat wordt behandeld in
Claude Code en Codex
afschermen.
Aan de kant van Claude Code
De declaratie past in één commando. De scope
project schrijft een .mcp.json in de root van het project, gedeeld met
het team, de scope local houdt hem voor jezelf.
claude mcp add --transport http wp http://127.0.0.1:8099/mcp \
--header "Authorization: Bearer $GK_MCP_TOKEN" --scope project{
"mcpServers": {
"wp": {
"type": "http",
"url": "http://127.0.0.1:8099/mcp",
"headers": { "Authorization": "Bearer ${GK_MCP_TOKEN}" }
}
}
}Twee voorzorgsmaatregelen. Het commando schrijft het token in platte tekst weg
in een bestand dat bedoeld is om te versiebeheren: vervang de waarde met de hand door
${GK_MCP_TOKEN}, zoals hierboven (de documentatie accepteert ook
${VAR:-standaard}). Verder is een server die in projectscope is toegevoegd niet actief
zolang niemand hem heeft goedgekeurd: claude mcp list toont
Pending approval tot de eerste sessie.
In een bedrijf laat de instelling managedMcpServers, verschenen in Claude Code
2.1.259 op 2 september 2026, toe om HTTP MCP-servers naar alle werkplekken te pushen, in hetzelfde
formaat als .mcp.json: daar hoort een alleen-lezend eindpunt thuis, eerder dan in
ieders eigen repository.
Welk gebruik, en waar houd je op?
Drie lezingen volstaan voor veel redactioneel werk. Eerst de SEO-nalezing: de agent schakelt
search_posts en get_post achter elkaar, en werkt op de echte tekst in
plaats van op zijn herinnering aan de site. Dan de jacht op wees-content: get_post
geeft een veld liens_internes terug. Op een artikel van 2.064 woorden, gepubliceerd in
januari 2023, antwoordde Codex me “drie interne links”, de controle in SQL geeft er inderdaad
drie.
De voorbereiding van concepten, ten slotte, is het geval waarin je moet weerstaan. De
verleiding is om een vierde tool create_draft toe te voegen. Doe dat niet in hetzelfde
eindpunt: een server die schrijft, is een server waarvan elke aanroep goedgekeurd, gelogd en
omkeerbaar moet zijn. Als je erop staat, maak er dan een tweede server van, op een andere poort,
met zijn eigen token, en laat default_tools_approval_mode op prompt
staan. Dezelfde regel geldt voor PrestaShop, waar het Webservice-oppervlak nog groter is: zie
MCP voor PrestaShop.
Voor een site in productie blijft de officiële weg het juiste antwoord: installeer
WordPress/mcp-adapter, stel alleen je eigen abilities bloot met
'meta' => array( 'mcp' => array( 'public' => true ) ), en geef de agent een
eigen account, niet het jouwe, met een intrekbaar applicatiewachtwoord. Beide aanpakken vullen
elkaar aan: de adapter voor wat WordPress al kan, een eigen eindpunt voor wat je tot op de
millimeter wilt afbakenen.
Wat je moet onthouden
- De plugin
Automattic/wordpress-mcpis gearchiveerd sinds 19 januari 2026, de
officiële weg isWordPress/mcp-adapter, v0.6.1 van 13 augustus 2026. - Zonder gedeclareerde ability stelt een MCP-adapter niets bloot: de kern van WordPress 7.1
registreert er maar drie, encore/get-site-infogeeft nu al het
administratie-e-mailadres terug. - Een eigen eindpunt van 152 regels brengt 154 REST-routes en 296.774 bytes schema terug tot
drie tools en 1.081 bytes. - Het echte slot is niet de WordPress-rol maar het filter
query, vooraf
geregistreerd vóórwp-load.php: elke schrijfactie wordt een gelogde
SELECT 1. - In
codex execwordt een tool zonderreadOnlyHintgeweigerd: het is
de annotatie, en niets anders, die de automatische aanroep in moduswritestoestaat. - Onder Apache en mod_php is
$_SERVER['HTTP_AUTHORIZATION']leeg: lees de header
metgetallheaders(), anders antwoordt je server met 401 zonder duidelijke reden.
Veelgemaakte fouten
/wp-json/wp-abilities/v1/abilities geeft 401 terug voor een anonieme gebruiker, en een ability gemarkeerd als readonly weigert de POST op zijn route /run met een 405. Gebruik GET en een geauthenticeerd account.mod_php is $_SERVER['HTTP_AUTHORIZATION'] leeg, terwijl getallheaders() hem wel teruggeeft. Lees allebei uit, anders antwoordt je eindpunt met 401 zonder duidelijke reden.claude mcp add --header schrijft de waarde letterlijk weg in een bestand dat bedoeld is om te versiebeheren. Vervang ze door ${GK_MCP_TOKEN} en bewaar het geheim in de omgeving.readOnlyHint weigert Codex de aanroep in codex exec, zelfs met default_tools_approval_mode = "writes". Geverifieerd met een negatieve controle: het is de annotatie die het vrijgeeft, niets anders.query, vooraf geregistreerd in $wp_filter vóór wp-load.php.

