Um agente de IA não deveria receber acesso genérico ao painel, ao banco ou a todas as rotas REST do WordPress. Ele precisa de ferramentas pequenas, descritas por contratos e limitadas às operações necessárias. A Abilities API organiza esse catálogo no WordPress; um adaptador MCP pode apresentá-lo ao agente.
Neste tutorial vamos construir e testar o WP Abilities MCP Bridge, um exemplo em Node.js que:
- autentica no WordPress com uma Application Password;
- descobre as Abilities pela REST API;
- aplica uma allowlist local;
- bloqueia escrita e destruição por padrão;
- converte o
input_schemaem schema de ferramenta; - registra uma ferramenta MCP por Ability autorizada;
- executa a chamada sem entregar a credencial ao modelo.
O código foi validado com Node.js 22, SDK TypeScript MCP 2.0 e WordPress 7.1-RC1 em Docker. O fluxo real aprovou execução como administrador, negação como assinante e recusa depois da revogação da credencial.
O papel de cada camada
A integração tem quatro fronteiras diferentes:
- o agente escolhe uma ferramenta MCP pelo nome, descrição e schema;
- o bridge restringe quais Abilities podem virar ferramentas;
- a Application Password identifica um usuário no WordPress;
- a
permission_callbackda Ability decide se aquele usuário pode executar a operação sobre aquele objeto.
MCP não substitui a autorização do WordPress. A allowlist também não substitui a permission_callback. As duas proteções se complementam: uma reduz o catálogo exposto ao agente; a outra aplica a política no sistema que possui os dados.

Pré-requisitos
- WordPress 6.9 ou superior com uma Ability exposta em REST;
- HTTPS no ambiente de produção;
- usuário de integração com o menor privilégio possível;
- Application Password exclusiva para esse bridge;
- Node.js 20 ou superior.
Usaremos wp24/get-post-summary, criada nos artigos anteriores. Ela recebe post_id, exige que o usuário possa editar o post e retorna somente campos previstos no output_schema.
Estrutura do projeto
O projeto distribuível acompanha este artigo e usa esta organização:
wp-abilities-mcp-bridge/
|-- src/
| |-- config.mjs
| |-- schema.mjs
| |-- server.mjs
| `-- wordpress-client.mjs
|-- test/
|-- .env.example
`-- package.json
Instale as dependências:
npm install
Baixe gratuitamente o WP Abilities MCP Bridge 0.1.0 e extraia o pacote em uma pasta local antes de executar os comandos. SHA-256: D15A87875776C208A42081EEAE901856831A08722A69FB92FF972FA4E5E00988.
O exemplo usa os pacotes oficiais separados da linha 2.0 do SDK:
{
"dependencies": {
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.0.0"
}
}
Configuração sem colocar segredos no código
O bridge lê a configuração do ambiente:
WP_URL=https://example.com
WP_USERNAME=agente-editorial
WP_APPLICATION_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
WP_ABILITY_ALLOWLIST=wp24/get-post-summary,wp24/site-summary
WP_ALLOW_MUTATIONS=false
WP_ABILITY_ALLOWLIST é obrigatória. Fora de localhost, WP_URL precisa usar HTTPS. Mutações ficam bloqueadas enquanto WP_ALLOW_MUTATIONS não for explicitamente habilitada.
Não passe a Application Password em argumentos de linha de comando nem a inclua no arquivo de configuração versionado. Configure-a no gerenciador de secrets do processo que inicia o servidor MCP.
Cliente da REST API
O cliente mantém o cabeçalho Basic Auth dentro do processo:
function basicAuth(username, password) {
return `Basic ${Buffer.from(`${username}:${password}`, 'utf8').toString('base64')}`;
}
export class WordPressAbilitiesClient {
constructor(config, fetchImpl = fetch) {
this.config = config;
this.fetch = fetchImpl;
this.authorization = basicAuth(config.username, config.applicationPassword);
}
async discover() {
return this.request('/wp-json/wp-abilities/v1/abilities');
}
}
O segredo não entra na URL, no nome da ferramenta nem na resposta enviada ao agente. O bridge também não escreve logs em stdout, porque o transporte stdio reserva esse canal para mensagens MCP.
GET, POST e DELETE conforme as anotações
O método HTTP é escolhido pelas anotações da Ability:
async execute(ability, input) {
const annotations = ability.meta?.annotations ?? {};
const path = `/wp-json/wp-abilities/v1/abilities/${ability.name}/run`;
if (annotations.destructive === true) {
return this.request(withQuery(path, input), { method: 'DELETE' });
}
if (annotations.readonly === true) {
return this.request(withQuery(path, input), { method: 'GET' });
}
return this.request(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input }),
});
}
No RC1 testado, a entrada GET parametrizada funcionou como input[post_id]=4. Confirme esse detalhe contra a versão final e a documentação corrente do endpoint.
A allowlist é uma fronteira local
Descobrir uma Ability não significa que ela deve ser entregue ao agente. A seleção exige nome autorizado e, por padrão, somente leitura não destrutiva:
export function selectAllowedAbilities(abilities, config) {
return abilities.filter((ability) => {
if (!config.allowlist.has(ability.name)) return false;
const annotations = ability.meta?.annotations ?? {};
if (!config.allowMutations && (
annotations.readonly !== true ||
annotations.destructive === true
)) return false;
return true;
});
}
Essa política é deliberadamente conservadora. Uma Ability sem readonly: true não é tratada como leitura por suposição.
Transformando uma Ability em ferramenta MCP
Depois da descoberta, cada Ability aprovada é registrada no servidor:
server.registerTool(
'wp_wp24__get-post-summary',
{
title: ability.label,
description: ability.description,
inputSchema: jsonSchemaToZod(ability.input_schema),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
},
},
async (input) => {
const result = await client.execute(ability, input);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
structuredContent: result,
};
},
);
O nome é normalizado para evitar colisões entre namespace e nome. wp24/get-post-summary vira wp_wp24__get-post-summary.
O input_schema é convertido para Zod no bridge. O exemplo cobre objetos, arrays, strings, números, inteiros, booleanos, nulos, enums, campos obrigatórios, limites e additionalProperties: false. Um produto genérico deve decidir explicitamente como tratar palavras-chave de JSON Schema que ainda não suporta.
Servindo por stdio
O SDK recebe uma fábrica assíncrona, permitindo descobrir as Abilities antes de aceitar chamadas:
serveStdio(() => buildServer());
Se a credencial não autenticar ou nenhuma Ability autorizada for descoberta, o processo falha fechado: o agente não recebe um servidor vazio que pareça saudável.
Configurando o cliente MCP
O formato exato varia entre clientes, mas a configuração conceitual é:
{
"mcpServers": {
"wordpress-wp24": {
"command": "node",
"args": ["CAMINHO/ABSOLUTO/wp-abilities-mcp-bridge/src/server.mjs"],
"env": {
"WP_URL": "https://example.com",
"WP_USERNAME": "agente-editorial",
"WP_APPLICATION_PASSWORD": "SEGREDO_NO_GERENCIADOR_DO_CLIENTE",
"WP_ABILITY_ALLOWLIST": "wp24/get-post-summary"
}
}
}
}
Prefira a integração de secrets oferecida pelo cliente ou pelo sistema operacional. O JSON acima mostra os nomes das variáveis, não recomenda gravar o segredo em texto puro.
O que o agente recebe
Com uma única Ability na allowlist, o teste MCP listou somente:
wp_wp24__get-post-summary
Uma chamada com post_id: 4 retornou conteúdo estruturado com ID, título, status, tipo, URL e resumo. O agente não recebeu senha de aplicativo, cabeçalho Authorization, cookies do painel, acesso arbitrário à REST API ou ferramentas fora da allowlist.
Teste real no WordPress 7.1-RC1
O ambiente isolado usou WordPress 7.1-RC1, PHP 8.3 e MariaDB. O mesmo bridge foi iniciado três vezes com credenciais efêmeras:
| Cenário | Descoberta MCP | Execução |
|---|---|---|
| administrador autorizado | ferramenta listada | sucesso |
| assinante autenticado | ferramenta listada | erro rest_ability_cannot_execute |
| Application Password revogada | inicialização recusada | não executada |
Esse resultado separa autenticação de autorização. O assinante provou sua identidade e conseguiu descobrir o contrato, mas a permission_callback negou o post. Depois da revogação, nem a descoberta autenticada foi aceita.
Também encontramos dois diagnósticos úteis durante o ensaio:
- uma instalação com links permanentes simples pode não resolver
/wp-json/...; teste a rota antes de culpar o bridge; - para habilitar Application Passwords somente em um ambiente HTTP local de teste, o filtro aplicável é
wp_is_application_passwords_available. Nunca replique esse bypass em produção.
Testes automatizados
Execute:
npm test
A suíte cobre configuração, HTTPS, allowlist, bloqueio de mutações, conversão de schema, métodos HTTP, erros REST, ausência de segredo em URLs e uma conexão MCP stdio completa com listagem e chamada de ferramenta.
Checklist para produção
- use HTTPS entre bridge e WordPress;
- crie um usuário exclusivo por integração e ambiente;
- conceda o menor conjunto de capabilities;
- use uma Application Password exclusiva e revogável;
- mantenha a allowlist curta e revisada;
- comece somente com Abilities de leitura;
- trate anotação ausente como risco, não como permissão;
- preserve
permission_callbackespecífica no WordPress; - valide entrada e saída com schemas restritivos;
- nunca registre
Authorizationou o segredo; - monitore falhas, duração e usuário no lado WordPress;
- teste negação e revogação antes da implantação;
- revise proxies que removem o cabeçalho
Authorization; - separe bridges de produção, homologação e desenvolvimento.
Próximo passo
O bridge fecha o fluxo técnico: descoberta, seleção, autenticação, autorização, execução e revogação. A próxima etapa da série vai aplicar o mesmo método à migração de uma função real de plugin, incluindo inventário, classificação de risco e desenho do contrato da Ability.



