
Existem três vias para ligar um agente ao WordPress: a Abilities API do núcleo (só três abilities), o adaptador oficial WordPress/mcp-adapter (v0.6.1 de 13 de agosto de 2026), e um ponto de entrada caseiro. A terceira é a única em que decides tudo: 152 linhas de PHP, três ferramentas de leitura, um token dedicado e um filtro SQL que transforma toda a escrita em SELECT 1. Testada aqui com o curl e depois a partir do Codex, contra um WordPress 7.1 local.
Ligar um agente de código ao WordPress significa dar-lhe acesso aos teus artigos, aos teus
rascunhos e, muito depressa, ao direito de escrever. Entre a palavra-passe de administrador e a
recusa pura e simples, existe uma posição sustentável: um ponto de entrada MCP que escreves tu
mesmo, que só expõe três leituras e que é incapaz de escrever na base de dados, mesmo que o modelo
o peça.
Este artigo fica rente ao WordPress. Para o protocolo em si, JSON-RPC, transportes, ciclo
initialize, vai ler
criar um servidor MCP em PHP,
aqui fala-se de wp-load.php, de $wpdb e de palavras-passe de aplicação.
O que propõe o WordPress em setembro de 2026
Existem três vias, e não se excluem. O artigo
From
Abilities to AI agents, publicado a 4 de fevereiro de 2026 em developer.wordpress.org, estabelece
a doutrina. Eis o seu estado a 7 de setembro de 2026, verificado repositório a repositório.
| Via | Estado | O que exige |
|---|---|---|
| Abilities API do núcleo | No WordPress desde a 6.9, três abilities registadas no meu WordPress 7.1 | Nada a instalar, mas nada é exposto em MCP sozinho |
Adaptador MCP oficial (WordPress/mcp-adapter) | Ativo: v0.6.1 de 13 de agosto de 2026, último envio a 2 de setembro de 2026, 1 672 estrelas | Um plugin ou um composer require, depois uma palavra-passe de aplicação ou OAuth 2.1 |
Plugin Automattic/wordpress-mcp | Arquivado: último envio a 30 de agosto de 2025, repositório só de leitura desde 19 de janeiro de 2026 | Já nada: remete para o adaptador oficial |
A mudança ainda não chegou a todo o lado: o
tutorial da LWS, publicado
a 2 de setembro de 2025, ainda descreve o plugin da Automattic como a via principal, quando o
WPFormation, atualizado
a 13 de agosto de 2026, já passou para o adaptador oficial. A API do GitHub decide:
archived: true de um lado, archived: false do outro.
O que contém a Abilities API do teu WordPress
A Abilities API é a base: um registo de capacidades declaradas, com um esquema de entrada, um
esquema de saída, uma categoria e um controlo de permissão. O adaptador MCP limita-se a traduzir
este registo em ferramentas MCP. Por outras palavras: sem ability, um adaptador MCP não expõe nada.
No WordPress 7.1 do meu ambiente local, o núcleo regista três. A primeira surpresa é que a rota
REST não está aberta:
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}}Como administrador, via WP-CLI e rest_do_request() para evitar ter de criar o
mínimo identificador, a lista cabe em três linhas:
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-infoDois detalhes contam para o que se segue. Cada ability traz desde logo anotações, e elas são vinculativas. core/get-site-info declara meta.annotations.readonly = true, e a rota de execução recusa então o POST:
# POST numa ability só de leitura
# HTTP 405: {"code":"rest_ability_invalid_method",
# "message":"Les « abilities » en lecture seule requièrent la méthode GET."}
# GET na mesma rota, como administrador: HTTP 200
# {"name":"Gekkode","url":"http://localhost:8090","admin_email":"c***@gekkode.com",
# "charset":"UTF-8","language":"fr-FR","version":"7.1"}O segundo detalhe lê-se na própria resposta, admin_email. A ability mais inofensiva do núcleo, a marcada «só de leitura, não destrutiva, idempotente», devolve o endereço de e-mail de administração do site. Um agente que a chama põe-no no seu contexto, e esse contexto vai parar a um fornecedor de modelo. É exatamente o tipo de fuga que não dispara alerta nenhum: nenhum dado foi alterado, tudo correu como previsto.
A mesma chamada como visitante anónimo devolve 401 rest_ability_cannot_execute: as permissões do WordPress são bem respeitadas. O modelo de permissões faz o seu trabalho. O problema vem da conta que dás ao agente, autorizada a ler tudo.
Porque escrever o teu próprio ponto de entrada?
Porque a superfície por defeito é imensa. Neste site, o índice da API REST declara
154 rotas para 296 774 bytes de esquema. Um agente que explora
esta superfície paga-a em tokens, e nada o impede de encontrar a rota que escreve.
A comparação mais eloquente incide sobre uma pesquisa idêntica:
| Chamada | Tamanho da resposta |
|---|---|
GET /wp/v2/posts?search=docker&per_page=5 | 116 227 bytes |
O mesmo com _fields=id,title,link,date | 1 080 bytes |
search_posts("docker", 5), a minha ferramenta MCP | 1 499 bytes |
Repara bem na terceira linha: a minha ferramenta é maior do que a API REST bem
parametrizada, porque conta as palavras a mais. O ganho não vem portanto do MCP, mas do facto de
a forma da resposta ser decidida uma vez, por ti, em vez de ser deixada ao modelo, que não tem
nenhuma razão para pensar em _fields. O fator 78 das chamadas em bruto é o que tu
fixas ao escrever a ferramenta.
Escrever o ponto de entrada só de leitura
O ficheiro tem 152 linhas de código efetivas. Carrega o wp-load.php, expõe três
ferramentas e coloca dois trincos. O primeiro é um token bearer dedicado, lido do ambiente: não
é uma palavra-passe do WordPress, nem uma palavra-passe de aplicação, e não dá acesso a nada além
destas três leituras.
$attendu = (string) getenv( 'GK_MCP_TOKEN' );
// Sob Apache/mod_php, $_SERVER['HTTP_AUTHORIZATION'] está vazio: o cabeçalho só chega
// por getallheaders(). Medido em 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;
}O segundo trinco é o coração do dispositivo. Restringir o papel do WordPress não basta:
qualquer extensão carregada durante o arranque pode escrever. Corta-se portanto mais abaixo, ao
nível do SQL. O WordPress passa cada consulta pelo filtro query
(wp-includes/class-wpdb.php), e sabe carregar filtros declarados antes
dele: wp-includes/plugin.php chama
WP_Hook::build_preinitialized_hooks( $wp_filter ) na linha 41. Instalamo-nos ali.
$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 */';
}
// Lido por WP_Hook::build_preinitialized_hooks() ao carregar 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';Qualquer consulta que não seja uma leitura é substituída por um SELECT 1 e registada.
Verifiquei o trinco com um DELETE voluntário, escrito para não corresponder a nenhuma
linha: se o filtro falhasse, nada seria destruído.
docker exec gekkode-mcp-lab php .../test-verrou.php
# Escritas tentadas durante o arranque do WordPress: 0
# Depois de um DELETE voluntário, consulta realmente enviada: SELECT 1 /* écriture refusée */
# Escritas bloqueadas no total: 1
# - DELETE FROM gk_options WHERE option_id = 0Zero escritas no arranque: neste site, o WordPress não tenta nada na base de dados durante um
simples carregamento. É uma boa notícia, não uma garantia: acrescenta uma extensão e a contagem
muda. O trinco serve para esse dia.
Restam as três ferramentas. São voluntariamente pobres: search_posts só vê
os artigos publicados, get_post recusa tudo o que não seja um artigo publicado, e
site_stats conta. Cada uma está anotada com readOnlyHint, vais ver mais
adiante que isso não é decorativo.
case 'search_posts':
$q = new WP_Query( array(
'post_type' => 'post',
'post_status' => 'publish', // nunca os rascunhos
's' => (string) ( $args['query'] ?? '' ),
'posts_per_page' => min( 20, max( 1, (int) ( $args['per_page'] ?? 5 ) ) ),
'no_found_rows' => true,
) );
// depois, para cada artigo: ID, título, link permanente, data, número de palavras.Lançar o ponto de entrada e verificá-lo com o curl
O servidor HTTP integrado do PHP chega, e tem uma vantagem: o ponto de entrada escuta numa
porta distinta da do site, só na loopback local. Nada é exposto publicamente. Aqui, tudo corre
num contentor descartável que se junta à rede Docker do blog.
# O token nunca é escrito num ficheiro do repositório.
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.phpO primeiro teste é o da recusa.
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'
# 401Depois o aperto de mão, como o faria um cliente MCP:
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}Os tempos de resposta, medidos neste espelho local de 1 198 artigos: 0,11 s para
tools/list, 0,02 s para site_stats, 0,07 s para
search_posts, 0,03 s para get_post. O essencial é o arranque do
WordPress, não o pedido.
E o controlo que conta: pedir um rascunho.
{"jsonrpc":"2.0","id":6,"result":{"isError":true,
"content":[{"type":"text","text":"article introuvable ou non publié"}]}}Ligá-lo ao Codex
O Codex guarda os seus servidores MCP em ~/.codex/config.toml. A opção -c evita mexer nesse ficheiro passando toda a declaração em linha, foi o que fiz aqui, com
codex-cli 0.153.4 e o 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."Primeira tentativa: falha instrutiva. O servidor liga-se, as ferramentas são descobertas, mas
as chamadas param secas.
mcp: wp/site_stats started
mcp: wp/site_stats (failed)
MCP tool call requires approval, but approval policy is neverEm sessão interativa, o Codex pedir-te-ia para aprovares cada chamada. Em
codex exec, ninguém pode responder: a política vale never,
a chamada é recusada. A definição ajusta-se por servidor:
[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 é o bom compromisso: só as ferramentas anunciadas como não modificadoras
passam sem pedir. Com esta definição e as anotações no lugar, a execução conclui-se em
vinte segundos para 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 »Quis saber o que, exatamente, desencadeava esta autorização. Voltei portanto a usar o mesmo ficheiro despojado das suas anotações, sem mudar mais nada: a mesma chamada voltou a ser MCP tool call requires approval. O que desencadeia não é portanto nem o nome da ferramenta nem a sua descrição, mas o sinalizador que o próprio servidor declara.
'annotations' => array(
'readOnlyHint' => true,
'destructiveHint' => false,
'idempotentHint' => true,
'openWorldHint' => false,
),Retém o sentido da dependência: é o teu servidor que afirma ser inofensivo, e o
cliente que acredita nisso. Um servidor MCP de terceiros pode mentir nessa linha. O raciocínio vale
portanto para código que escreveste, não para um servidor obtido numa marketplace, assunto
tratado em isolar o
Claude Code e o Codex.
Do lado do Claude Code
A declaração cabe num comando. O âmbito
project escreve um .mcp.json na raiz do projeto, partilhado pela
equipa, o âmbito local guarda-o só para ti.
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}" }
}
}
}Duas precauções. O comando escreve o token em claro num ficheiro feito para
ser versionado: substitui o valor por ${GK_MCP_TOKEN} à mão, como acima
(a documentação também aceita ${VAR:-defeito}). Depois, um servidor acrescentado em
âmbito de projeto não fica ativo enquanto ninguém o aprovar: claude mcp list mostra
Pending approval até à primeira sessão.
Em empresa, a definição managedMcpServers aparecida no Claude Code 2.1.259 a
2 de setembro de 2026 permite empurrar servidores MCP HTTP para todos os postos, no mesmo formato
que .mcp.json: é aí que um ponto de entrada só de leitura tem o seu lugar, em vez de
no repositório de cada um.
Que usos, e onde parar?
Três leituras chegam para muito trabalho editorial. A revisão SEO primeiro: o agente encadeia
search_posts e get_post, e trabalha sobre o texto real em vez da sua
memória do site. A caça a conteúdos órfãos depois: get_post devolve um campo
liens_internes. Num artigo de 2 064 palavras publicado em janeiro de 2023, o Codex
respondeu-me «três ligações internas», a verificação em SQL dá de facto três.
A preparação de rascunhos, por fim, é o caso em que é preciso resistir. A tentação é acrescentar
uma quarta ferramenta create_draft. Não o faças no mesmo ponto de entrada: um servidor
que escreve é um servidor em que cada chamada deve ser aprovada, registada e reversível. Se
insistires, faz dele um segundo servidor, noutra porta, com o seu próprio token, e deixa
default_tools_approval_mode em prompt. A mesma regra vale para o
PrestaShop, onde a superfície Webservice é ainda mais larga: vê
MCP para PrestaShop.
Para um site em produção, a via oficial continua a ser a boa resposta: instala o
WordPress/mcp-adapter, expõe só as tuas próprias abilities com
'meta' => array( 'mcp' => array( 'public' => true ) ), e dá ao agente um
conta dedicada, não a tua, com uma palavra-passe de aplicação revogável. As duas abordagens
complementam-se: o adaptador para o que o WordPress já sabe fazer, um ponto de entrada caseiro
para o que queres limitar ao milímetro.
A reter
- O plugin
Automattic/wordpress-mcpestá arquivado desde 19 de janeiro de 2026, a
via oficial é oWordPress/mcp-adapter, v0.6.1 de 13 de agosto de 2026. - Sem ability declarada, um adaptador MCP não expõe nada: o núcleo do WordPress 7.1 só
regista três, ecore/get-site-infojá devolve o endereço de e-mail de
administração. - Um ponto de entrada caseiro de 152 linhas reduz 154 rotas REST e 296 774 bytes de esquema a
três ferramentas e 1 081 bytes. - O verdadeiro trinco não é o papel do WordPress mas o filtro
querypré-registado
antes dowp-load.php: toda a escrita se torna umSELECT 1registado. - Em
codex exec, uma ferramenta semreadOnlyHinté recusada: é
a anotação, e mais nada, que autoriza a chamada automática em modowrites. - Sob Apache e mod_php,
$_SERVER['HTTP_AUTHORIZATION']está vazio: lê
o cabeçalho comgetallheaders(), senão o teu servidor responderá 401 sem razão
aparente.
Erros frequentes
/wp-json/wp-abilities/v1/abilities devolve 401 em anónimo, e uma ability marcada readonly recusa o POST na sua rota /run com um 405. Passa por GET e por uma conta autenticada.mod_php, $_SERVER['HTTP_AUTHORIZATION'] está vazio enquanto getallheaders() o devolve. Lê os dois, senão o teu ponto de entrada responde 401 sem nada que o explique.claude mcp add --header escreve o valor tal como está num ficheiro feito para ser versionado. Substitui-o por ${GK_MCP_TOKEN} e guarda o segredo no ambiente.readOnlyHint, o Codex recusa a chamada em codex exec mesmo com default_tools_approval_mode = "writes". Verificado por controlo negativo: é a anotação que desbloqueia, mais nada.query pré-registado em $wp_filter antes do wp-load.php.

