MCP para WordPress, sem dar as chaves do site a um agente

Três vias para ligar o Claude Code ou o Codex ao WordPress, e um ponto de entrada MCP caseiro de 152 linhas, só de leitura, escrito e executado contra um WordPress 7.1 local.

MCP para WordPress, sem dar as chaves do site a um agente
Resposta rápida

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:

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

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:

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

Dois 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:

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

php
$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.

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

// 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.

bash
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 = 0

Zero 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.

php
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.

bash
# 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.php

O primeiro teste é o da recusa.

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

Depois o aperto de mão, como o faria um cliente 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}

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.

json
{"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.

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

Primeira tentativa: falha instrutiva. O servidor liga-se, as ferramentas são descobertas, mas
as chamadas param secas.

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

Em 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:

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 é 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:

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 »

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.

php
'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.

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

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-mcp está arquivado desde 19 de janeiro de 2026, a
    via oficial é o WordPress/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, e core/get-site-info já 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 query pré-registado
    antes do wp-load.php: toda a escrita se torna um SELECT 1 registado.
  • Em codex exec, uma ferramenta sem readOnlyHint é recusada: é
    a anotação, e mais nada, que autoriza a chamada automática em modo writes.
  • Sob Apache e mod_php, $_SERVER['HTTP_AUTHORIZATION'] está vazio: lê
    o cabeçalho com getallheaders(), senão o teu servidor responderá 401 sem razão
    aparente.

Erros frequentes

Achar que a Abilities API está aberta A rota /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.
Deixar o Apache engolir o cabeçalho Authorization Sob 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.
Colar o token em claro no .mcp.json 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.
Esquecer as anotações de ferramenta Sem 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.
Contar com o papel do WordPress para proibir a escrita Um papel limita o utilizador, não o código carregado durante o arranque. O trinco fiável é o filtro query pré-registado em $wp_filter antes do wp-load.php.

Claude CodeCodexMCPPHPSécuritéWordPress

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.