MCP dla WordPressa, bez oddawania agentowi kluczy do witryny

Trzy drogi, żeby podłączyć Claude Code albo Codex do WordPressa, i domowy punkt wejścia MCP liczący 152 linie, tylko do odczytu, napisany i uruchomiony na lokalnym WordPressie 7.1.

MCP dla WordPressa, bez oddawania agentowi kluczy do witryny
Szybka odpowiedź

Istnieją trzy drogi, żeby podłączyć agenta do WordPressa: Abilities API rdzenia (tylko trzy abilities), oficjalny adapter WordPress/mcp-adapter (v0.6.1 z 13 sierpnia 2026), i domowy punkt wejścia. Trzecia to jedyna, w której Ty decydujesz o wszystkim: 152 linie PHP, trzy narzędzia do odczytu, dedykowany token i filtr SQL, który zamienia każdy zapis w SELECT 1. Przetestowana tutaj curlem, a potem z poziomu Codex, na lokalnym WordPressie 7.1.

Podłączenie agenta kodu do WordPressa oznacza danie mu dostępu do Twoich artykułów, szkiców i, bardzo szybko, prawa do zapisu. Między hasłem administratora a czystą odmową istnieje stanowisko, które da się utrzymać: punkt wejścia MCP, który piszesz sam, który udostępnia tylko trzy odczyty i który nie jest w stanie zapisać niczego w bazie, nawet jeśli model o to poprosi.

Ten artykuł trzyma się blisko WordPressa. Po sam protokół, JSON-RPC, transporty, cykl
initialize, przeczytaj
stworzyć serwer MCP w PHP,
tutaj mówimy o wp-load.php, o $wpdb i o hasłach aplikacji.

Co oferuje WordPress we wrześniu 2026

Istnieją trzy drogi, i nie wykluczają się nawzajem. Wpis
From
Abilities to AI agents
, opublikowany 4 lutego 2026 na developer.wordpress.org, ustala doktrynę.
Oto ich stan na 7 września 2026, zweryfikowany repozytorium po repozytorium.

Droga Stan Czego wymaga
Abilities API rdzenia W WordPressie od wersji 6.9, trzy abilities zarejestrowane na moim WordPressie 7.1 Nic do zainstalowania, ale nic nie jest samo z siebie udostępnione w MCP
Oficjalny adapter MCP (WordPress/mcp-adapter) Aktywny: v0.6.1 z 13 sierpnia 2026, ostatni commit 2 września 2026, 1 672 gwiazdek Wtyczka albo composer require, potem hasło aplikacji albo OAuth 2.1
Wtyczka Automattic/wordpress-mcp Zarchiwizowana: ostatni commit 30 sierpnia 2025, repozytorium tylko do odczytu od 19 stycznia 2026 Już nic: odsyła do oficjalnego adaptera

Przełącznik nie wszędzie jeszcze zadziałał:
tutorial LWS, opublikowany
2 września 2025, wciąż opisuje wtyczkę Automattic jako główną drogę, podczas gdy
WPFormation, zaktualizowany
13 sierpnia 2026, przeszedł na oficjalny adapter. API GitHuba rozstrzyga sprawę:
archived: true z jednej strony, archived: false z drugiej.

Co zawiera Abilities API Twojego WordPressa

Abilities API to fundament: rejestr zadeklarowanych możliwości, ze schematem wejścia, schematem
wyjścia, kategorią i kontrolą uprawnień. Adapter MCP tylko tłumaczy ten rejestr na narzędzia MCP.
Innymi słowy: bez ability adapter MCP niczego nie udostępnia.

Na WordPressie 7.1 mojego lokalnego środowiska rdzeń rejestruje trzy. Pierwsza niespodzianka
jest taka, że trasa REST nie jest otwarta:

bash
curl -s http://localhost:8090/wp-json/wp-abilities/v1/abilities
# {"code":"rest_forbidden","message":"Désolé, vous n'avez pas l'autorisation de faire cela.",
#  "data":{"status":401}}

Jako administrator, przez WP-CLI i rest_do_request(), żeby uniknąć tworzenia
jakiegokolwiek identyfikatora, lista mieści się w trzech liniach:

bash
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-info

Dwa szczegóły liczą się na później. Każda ability niesie przede wszystkim adnotacje, i są one wiążące. core/get-site-info deklaruje meta.annotations.readonly = true, a trasa wykonania odmawia wtedy POST-a:

bash
# POST na ability tylko do odczytu
# HTTP 405: {"code":"rest_ability_invalid_method",
#             "message":"Les « abilities » en lecture seule requièrent la méthode GET."}

# GET na tej samej trasie, jako administrator: HTTP 200
# {"name":"Gekkode","url":"http://localhost:8090","admin_email":"c***@gekkode.com",
#  "charset":"UTF-8","language":"fr-FR","version":"7.1"}

Drugi szczegół czyta się w samej odpowiedzi, admin_email. Najbardziej niewinna ability rdzenia, ta oznaczona jako „tylko do odczytu, nieniszcząca, idempotentna”, zwraca adres e-mail administracji witryny. Agent, który ją wywołuje, umieszcza go w swoim kontekście, a ten kontekst trafia do dostawcy modelu. To dokładnie ten rodzaj wycieku, który nie wywołuje żadnego alarmu: żadne dane nie zostały zmodyfikowane, wszystko przebiegło zgodnie z planem.

To samo wywołanie jako anonimowy odwiedzający zwraca 401 rest_ability_cannot_execute: uprawnienia WordPressa są rzeczywiście przestrzegane. Model uprawnień robi swoje. Problem bierze się z konta, które dajesz agentowi, z prawem do czytania wszystkiego.

Dlaczego napisać własny punkt wejścia?

Bo domyślna powierzchnia jest ogromna. Na tej witrynie indeks API REST deklaruje
154 trasy na 296 774 bajty schematu. Agent, który eksploruje tę
powierzchnię, płaci za nią tokenami, i nic nie przeszkadza mu znaleźć trasy, która zapisuje.

Najbardziej wymowne porównanie dotyczy identycznego wyszukiwania:

Wywołanie Rozmiar odpowiedzi
GET /wp/v2/posts?search=docker&per_page=5 116 227 bajtów
To samo z _fields=id,title,link,date 1 080 bajtów
search_posts("docker", 5), moje narzędzie MCP 1 499 bajtów

Przeczytaj uważnie trzecią linię: moje narzędzie jest większe niż dobrze
sparametryzowane API REST, bo dodatkowo liczy słowa. Zysk nie bierze się więc z MCP, tylko z
faktu, że forma odpowiedzi jest ustalana raz, przez Ciebie, zamiast być pozostawiona modelowi,
który nie ma żadnego powodu, żeby pomyśleć o _fields. Współczynnik 78 z surowych
wywołań to coś, co Ty ustalasz, pisząc narzędzie.

Napisać punkt wejścia tylko do odczytu

Plik ma 152 linie rzeczywistego kodu. Ładuje wp-load.php, udostępnia trzy
narzędzia i zakłada dwie blokady. Pierwsza to dedykowany token bearer, czytany ze środowiska: to
nie jest ani hasło WordPressa, ani hasło aplikacji, i nie daje dostępu do niczego poza tymi trzema
odczytami.

php
$attendu = (string) getenv( 'GK_MCP_TOKEN' );

// Pod Apache/mod_php, $_SERVER['HTTP_AUTHORIZATION'] jest puste: nagłówek dociera
// tylko przez getallheaders(). Zmierzone na 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;
}

Druga blokada to serce całego mechanizmu. Ograniczenie roli WordPressa nie wystarczy: dowolne
rozszerzenie załadowane podczas uruchamiania może zapisywać. Tniemy więc niżej, na poziomie SQL.
WordPress przepuszcza każde zapytanie przez filtr query
(wp-includes/class-wpdb.php), i potrafi ładować filtry zadeklarowane przed
nim: wp-includes/plugin.php wywołuje
WP_Hook::build_preinitialized_hooks( $wp_filter ) w linii 41. Tam się instalujemy.

php
$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 */';
}

// Czytane przez WP_Hook::build_preinitialized_hooks() przy ładowaniu 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';

Każde zapytanie, które nie jest odczytem, jest zastępowane przez SELECT 1 i
logowane. Zweryfikowałem blokadę celowym DELETE, napisanym tak, żeby nie pasował do
żadnego wiersza: gdyby filtr zawiódł, nic nie zostałoby zniszczone.

bash
docker exec gekkode-mcp-lab php .../test-verrou.php

# Próby zapisu podczas uruchamiania WordPressa: 0
# Po celowym DELETE, żądanie faktycznie wysłane: SELECT 1 /* écriture refusée */
# Łącznie zablokowanych zapisów: 1
#   - DELETE FROM gk_options WHERE option_id = 0

Zero zapisów przy uruchamianiu: na tej witrynie WordPress niczego nie próbuje w bazie podczas
zwykłego ładowania. To dobra wiadomość, nie gwarancja: dodaj rozszerzenie, a liczba się zmieni.
Blokada służy właśnie na ten dzień.

Pozostają trzy narzędzia. Są celowo ubogie: search_posts widzi tylko opublikowane
artykuły, get_post odrzuca wszystko, co nie jest opublikowanym artykułem, a
site_stats liczy. Każde jest oznaczone adnotacją readOnlyHint, zobaczysz
dalej, że to nie jest dekoracja.

php
case 'search_posts':
	$q = new WP_Query( array(
		'post_type'      => 'post',
		'post_status'    => 'publish',           // nigdy szkiców
		's'              => (string) ( $args['query'] ?? '' ),
		'posts_per_page' => min( 20, max( 1, (int) ( $args['per_page'] ?? 5 ) ) ),
		'no_found_rows'  => true,
	) );
	// a potem, dla każdego artykułu: ID, tytuł, permalink, data, liczba słów.

Uruchomienie punktu wejścia i weryfikacja curlem

Wbudowany serwer HTTP PHP wystarcza, i ma jedną zaletę: punkt wejścia nasłuchuje na porcie
innym niż port witryny, wyłącznie na pętli lokalnej. Nic nie jest udostępnione publicznie. U mnie
całość działa w jednorazowym kontenerze, który dołącza do sieci Docker bloga.

bash
# Token nigdy nie jest zapisywany w pliku repozytorium.
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.php

Pierwszy test to test odmowy.

bash
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'
# 401

Potem uzgadnianie, tak jak zrobiłby to klient MCP:

bash
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"}}}
bash
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}

Czasy odpowiedzi, zmierzone na tym lokalnym lustrze 1 198 artykułów: 0,11 s dla
tools/list, 0,02 s dla site_stats, 0,07 s dla search_posts,
0,03 s dla get_post. Najważniejsze jest uruchomienie WordPressa, nie samo zapytanie.

I kontrola, która się liczy: zażądanie szkicu.

json
{"jsonrpc":"2.0","id":6,"result":{"isError":true,
 "content":[{"type":"text","text":"article introuvable ou non publié"}]}}

Podłączenie go do Codex

Codex trzyma swoje serwery MCP w ~/.codex/config.toml. Opcja -c pozwala tego pliku nie ruszać, przekazując całą deklarację w linii poleceń, i tak właśnie zrobiłem tutaj, z codex-cli 0.153.4 i GPT-6 Astra.

bash
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."

Pierwsza próba: pouczająca porażka. Serwer się łączy, narzędzia są odkrywane, ale wywołania
zatrzymują się w miejscu.

bash
mcp: wp/site_stats started
mcp: wp/site_stats (failed)
MCP tool call requires approval, but approval policy is never

W sesji interaktywnej Codex poprosiłby Cię o zatwierdzenie każdego wywołania. W
codex exec nikt nie może odpowiedzieć: polityka ma wartość never,
wywołanie jest odrzucane. Ustawienie wprowadza się dla każdego serwera osobno:

toml
[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 = 20

writes to dobry kompromis: tylko narzędzia zapowiedziane jako niemodyfikujące
przechodzą bez pytania. Z tym ustawieniem i adnotacjami na miejscu, wykonanie kończy się sukcesem
w dwadzieścia sekund za 9 550 tokenów:

bash
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 »

Chciałem wiedzieć, co dokładnie wyzwalało tę autoryzację. Podałem więc ponownie ten sam plik, pozbawiony adnotacji, nie zmieniając niczego innego: to samo wywołanie znów stało się MCP tool call requires approval. Wyzwalaczem nie jest więc ani nazwa narzędzia, ani jego opis, tylko flaga, którą deklaruje sam serwer.

php
'annotations' => array(
	'readOnlyHint'    => true,
	'destructiveHint' => false,
	'idempotentHint'  => true,
	'openWorldHint'   => false,
),

Zapamiętaj kierunek tej zależności: to Twój serwer twierdzi, że jest nieszkodliwy, a klient mu
wierzy. Zewnętrzny serwer MCP może skłamać na tej linii. Rozumowanie to jest więc słuszne dla kodu,
który sam napisałeś, nie dla serwera pobranego z marketplace’u, temat poruszony w
izolowaniu Claude Code
i Codex
.

Po stronie Claude Code

Deklaracja mieści się w jednej komendzie.
Zakres project zapisuje .mcp.json w katalogu głównym projektu,
współdzielony przez zespół, zakres local zachowuje go dla Ciebie.

bash
claude mcp add --transport http wp http://127.0.0.1:8099/mcp \
  --header "Authorization: Bearer $GK_MCP_TOKEN" --scope project
json
{
  "mcpServers": {
    "wp": {
      "type": "http",
      "url": "http://127.0.0.1:8099/mcp",
      "headers": { "Authorization": "Bearer ${GK_MCP_TOKEN}" }
    }
  }
}

Dwa środki ostrożności. Komenda zapisuje token jawnym tekstem w pliku
stworzonym do wersjonowania: zastąp wartość przez ${GK_MCP_TOKEN} ręcznie, tak jak
powyżej (dokumentacja akceptuje też ${VAR:-domyslna}). Poza tym serwer dodany w
zakresie projektu nie jest aktywny, dopóki nikt go nie zatwierdzi: claude mcp list
pokazuje Pending approval aż do pierwszej sesji.

W firmie ustawienie managedMcpServers, które pojawiło się w Claude Code 2.1.259
2 września 2026, pozwala wypchnąć serwery MCP HTTP na wszystkie stanowiska, w tym samym formacie
co .mcp.json: to tam punkt wejścia tylko do odczytu ma swoje miejsce, zamiast w
repozytorium każdego z osobna.

Jakie zastosowania, i gdzie się zatrzymać?

Trzy odczyty wystarczają do sporej części pracy redakcyjnej. Najpierw korekta SEO: agent
wykonuje po kolei search_posts i get_post, i pracuje na rzeczywistym
tekście zamiast na swoim wspomnieniu witryny. Potem polowanie na treści osierocone:
get_post zwraca pole liens_internes. Na artykule liczącym 2 064 słowa,
opublikowanym w styczniu 2023, Codex odpowiedział mi „trzy linki wewnętrzne”, weryfikacja w SQL
daje rzeczywiście trzy.

Przygotowywanie szkiców to z kolei przypadek, w którym trzeba się opierać. Pokusa polega na
dodaniu czwartego narzędzia create_draft. Nie rób tego w tym samym punkcie wejścia:
serwer, który zapisuje, to serwer, którego każde wywołanie musi być zatwierdzone, zalogowane i
odwracalne. Jeśli Ci na tym zależy, zrób z tego drugi serwer, na innym porcie, z własnym tokenem,
i zostaw default_tools_approval_mode na prompt. Ta sama zasada dotyczy
PrestaShop, gdzie powierzchnia Webservice jest jeszcze szersza: zobacz
MCP dla PrestaShop.

Dla witryny na produkcji oficjalna droga pozostaje dobrą odpowiedzią: zainstaluj
WordPress/mcp-adapter, udostępniaj tylko własne abilities przez
'meta' => array( 'mcp' => array( 'public' => true ) ), i daj agentowi
dedykowane konto, nie swoje, z odwoływalnym hasłem aplikacji. Oba podejścia się uzupełniają:
adapter do tego, co WordPress już potrafi, domowy punkt wejścia do tego, co chcesz ograniczyć co
do milimetra.

Co warto zapamiętać

  • Wtyczka Automattic/wordpress-mcp jest zarchiwizowana od 19 stycznia 2026,
    oficjalną drogą jest WordPress/mcp-adapter, v0.6.1 z 13 sierpnia 2026.
  • Bez zadeklarowanej ability adapter MCP niczego nie udostępnia: rdzeń WordPressa 7.1 rejestruje
    ich tylko trzy, a core/get-site-info już zwraca adres e-mail administracji.
  • Domowy punkt wejścia liczący 152 linie sprowadza 154 trasy REST i 296 774 bajty schematu do
    trzech narzędzi i 1 081 bajtów.
  • Prawdziwą blokadą nie jest rola WordPressa, tylko filtr query zarejestrowany
    wcześniej niż wp-load.php: każdy zapis staje się zalogowanym SELECT 1.
  • W codex exec narzędzie bez readOnlyHint jest odrzucane: to adnotacja,
    i nic innego, autoryzuje automatyczne wywołanie w trybie writes.
  • Pod Apache i mod_php $_SERVER['HTTP_AUTHORIZATION'] jest puste: czytaj nagłówek
    przez getallheaders(), inaczej Twój serwer odpowie 401 bez wyraźnego powodu.

Częste błędy

Sądzić, że Abilities API jest otwarte Trasa /wp-json/wp-abilities/v1/abilities zwraca 401 anonimowo, a ability oznaczona jako readonly odmawia POST-a na swojej trasie /run kodem 405. Przejdź przez GET i przez uwierzytelnione konto.
Pozwolić Apache połknąć nagłówek Authorization Pod mod_php, $_SERVER['HTTP_AUTHORIZATION'] jest puste, podczas gdy getallheaders() go zwraca. Czytaj oba, inaczej Twój punkt wejścia odpowie 401 bez żadnego wytłumaczenia.
Wklejenie tokena jawnym tekstem do .mcp.json claude mcp add --header zapisuje wartość dosłownie w pliku stworzonym do wersjonowania. Zastąp ją przez ${GK_MCP_TOKEN} i trzymaj sekret w środowisku.
Zapomnienie o adnotacjach narzędzia Bez readOnlyHint Codex odmawia wywołania w codex exec, nawet z default_tools_approval_mode = "writes". Zweryfikowane kontrolą negatywną: to adnotacja odblokowuje, nic innego.
Liczenie na rolę WordPressa, żeby zakazać zapisu Rola ogranicza użytkownika, nie kod załadowany podczas uruchamiania. Niezawodną blokadą jest filtr query zarejestrowany w $wp_filter wcześniej niż wp-load.php.

Claude CodeCodexMCPPHPSécuritéWordPress

Damien Flandrin Web developer od 2010 roku, twórca Gekkode i Email Impact. Każdy artykuł jest sprawdzany na prawdziwym projekcie przed publikacją. Kontakt
Newsletter

Nowe testy, poradniki i projekty, e-mailem.

Powtarzalne testy, wersjonowany kod, datowane wyniki. Nigdy spamu.