docs: mapeamento completo do EMCP Tools (11 docs, ~6070 linhas)

Especificação funcional do plugin emcp-tools v3.12.1 (msrbuilds/elementor-mcp,
GPL-2.0-or-later) via leitura directa do código-fonte em emanuelalmeida.pt.

- 00: arquitectura (bootstrap, ability registrar, dispatcher, MCP adapter)
- 01: Elementor classico (paginas, layout, widgets, templates, globals)
- 02: Elementor Atomic v4 + Gutenberg
- 03: WordPress core (conteudo, media, settings, temas)
- 04: Themer (CPT, condicoes, render, PHP templates)
- 05: Redirects + change ledger unificado (rollback)
- 06: Sandbox PHP snippets + custom widgets
- 07: Filesystem/DB/WP-CLI/Security/Performance (maior risco)
- 08: Integracoes terceiros (ACF, Meta Box, forms, SEO)
- 09: Stock images + Cloud + OAuth
- 10: Sistema de modulos + inventario Pro-only (30 classes)
- INDEX: sintese, sequencia de construcao, tabela de risco

Produzido por 10 subagentes code-explorer em paralelo + revisao cruzada de
consistencia. Cada doc inclui blueprint de replica (copiar/simplificar/omitir).
This commit is contained in:
Claude Code
2026-08-19 06:41:04 +01:00
commit 8ada367bd0
12 changed files with 6301 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
# 00 — Arquitectura do EMCP Tools e blueprint da réplica
Fonte: leitura directa do código-fonte `emcp-tools` v3.12.1 (build Free), instalado em
`emanuelalmeida.pt` (`/home/ealmeida/emanuelalmeida.pt/wp-content/plugins/emcp-tools/`),
19-08-2026. Autor terceiro: Mian Shahzad Raza (`msrbuilds.com`), repo público
`github.com/msrbuilds/elementor-mcp`, licença **GPL-2.0-or-later**.
## 0. Achado mais importante: a peça MCP não é do fornecedor — é do WordPress core team
O plugin **não implementa o protocolo MCP**. Depende de duas bibliotecas Composer, ambas
**projectos oficiais da equipa de IA do WordPress.org** (`WordPress AI Team`,
`make.wordpress.org/ai`), GPL-2.0-or-later, públicas:
| Pacote | Namespace PHP | Repo | O que faz |
|---|---|---|---|
| `wordpress/mcp-adapter` | `WP\MCP\` | `github.com/wordpress/mcp-adapter` | Expõe abilities da Abilities API como servidor MCP (`McpAdapter`, `McpServer`, `HttpTransport`, `SessionManager`, `RequestRouter`, `HttpSessionValidator`, `McpErrorFactory`) — cria o endpoint REST `/wp-json/mcp/{route}` |
| `wordpress/php-mcp-schema` | `WP\McpSchema\` | `github.com/WordPress/php-mcp-schema` | DTOs PHP para o schema oficial do protocolo MCP |
A **Abilities API em si é núcleo do WordPress 6.9+/7.0** (`wp_register_ability()`,
`wp_get_ability()`, `wp_get_abilities()`, hooks `wp_abilities_api_categories_init` /
`wp_abilities_api_init`) — não é código do plugin nem do adapter.
**Consequência directa para a réplica:** não há nada de protocolo para reimplementar.
`composer require wordpress/mcp-adapter` (ou vendorizar uma cópia, como o EMCP faz — ver §1)
dá-nos a mesma máquina JSON-RPC/sessão/transporte. O trabalho real do "clone" é: (a) registar
as nossas próprias abilities via `wp_register_ability()`, com o nosso próprio prefixo (ex.
`descomplicar-tools/*`), e (b) `$mcp_adapter->create_server(...)` com o nosso `server_id`. Tudo o
resto — catálogo de ~165 tools, módulos, sandbox, change-ledger, content-mirror — é trabalho de
aplicação, não de protocolo.
**Nota de licenciamento:** por ser GPL-2.0-or-later, nada impede legalmente copiar/adaptar o
código-fonte do EMCP Tools directamente para um plugin próprio (fork), mantendo a licença GPL a
jusante. "Réplica 100% nossa" pode ser lida como (a) fork com rebranding + remoção do Freemius +
namespace próprio (muito menos esforço, reutiliza ~78k linhas já testadas), ou (b) reimplementação
limpa a partir desta especificação (mais esforço, zero dependência de código de terceiros,
liberdade total de redesenho). Esta série de documentos serve ambos os caminhos — é a
especificação funcional; a decisão de fork-vs-reescrita é independente dela.
## 1. Estrutura de directórios do plugin
```
emcp-tools/
├── emcp-tools.php # bootstrap: header, guarda Free⇄Pro, Freemius, handoff
├── includes/
│ ├── class-bootstrap.php # boot(): dependências, carrega classes, arranca Plugin
│ ├── class-plugin.php # EMCP_Tools_Plugin (singleton orquestrador) — ver §3
│ ├── class-mcp-adapter-bootstrap.php # preload do WP\MCP\ vendorizado — ver §2
│ ├── class-schema-compat.php # emcp_tools_register_ability() + normalização de schema
│ ├── abilities/
│ │ ├── class-ability-registrar.php # regista TODOS os grupos de abilities — índice mestre
│ │ ├── class-dispatcher-abilities.php # compact tool mode (list-tools/get-tool-schema/call-tool)
│ │ ├── class-{grupo}-abilities.php # ~40 classes de abilities (uma por grupo funcional)
│ │ ├── forms/class-{plugin}-integration.php # CF7 (free) + dispatcher genérico
│ │ └── seo/class-{plugin}-integration.php # SlimSEO (free) + dispatcher genérico
│ ├── modules/ # os 9 módulos opcionais + registry — ver doc 10
│ ├── themer/ # Themer CPT/render/condições/blocos/widgets/PHP — ver doc 04
│ ├── sandbox/ # armazenamento de artefactos sandbox (bundle/paths/store) — ver doc 06
│ ├── security/ # 4 auditores + finding — ver doc 07
│ ├── performance/ # 3 auditores + finding — ver doc 07
│ ├── redirects/ # handler + store — ver doc 05
│ ├── cloud/ # ligação/sync EMCP Cloud — ver doc 09
│ ├── oauth/ # servidor OAuth para autenticação MCP remota — ver doc 09
│ ├── wpcli/ # runner/validator/jobs para run-wp-cli — ver doc 07
│ ├── widgets/ # catálogos de widgets Elementor (free/woo/pro) — ver doc 02
│ ├── blocks-catalog/ # catálogos Kadence Blocks / Spectra — ver doc 03
│ ├── schemas/ # gerador de JSON Schema a partir de controls Elementor
│ ├── validators/ # validador de settings/elementos
│ ├── admin/ # UI de admin: class-admin.php (6115 linhas!), views/*, mcpb-builder
│ └── vendors/fremius/ # SDK Freemius (licenciamento/billing) — ~50k linhas, IRRELEVANTE p/ réplica
├── vendor/wordpress/ # mcp-adapter + php-mcp-schema vendorizados (ver §0)
├── vendor/enshrined/svg-sanitize/ # sanitização SVG (módulo svg-support)
├── vendor/automattic/jetpack-autoloader/ # autoloader partilhado entre plugins que usam o adapter
└── prompts/, assets/, bin/, languages/
```
**Total:** ~128 465 linhas PHP fora de `vendor/`/`languages/` (medido 19-08-2026); ~50k dessas
são o SDK Freemius bundled (irrelevante para a réplica — ver §6). Ficheiro maior de longe:
`includes/admin/class-admin.php`, **6115 linhas** (catálogo de tools para a UI, aplicação do
deny-list incremental de 34 versões, todas as views de admin — ver doc dedicado se necessário).
## 2. Cadeia de arranque (ordem real de hooks)
```
emcp-tools.php carrega no arquivo (topo do request)
↓ guarda Free⇄Pro (§7), depois Freemius init
↓ require class-bootstrap.php
add_action('plugins_loaded', [Bootstrap, 'boot'], 20)
↓
plugins_loaded:20 → EMCP_Tools_Bootstrap::boot()
↓ EMCP_Tools_Adapter_Bootstrap::ensure() — preload do WP\MCP\ vendorizado (ver §2.1)
↓ require de todas as classes includes/**/*.php
↓ EMCP_Tools_Plugin::instance() → init()
add_action('wp_abilities_api_categories_init', register_category)
add_action('wp_abilities_api_init', register_abilities) # regista ~165 abilities
add_action('mcp_adapter_init', register_mcp_server, 20) # cria o servidor MCP
add_filter('emcp_tools_ability_names', filter_disabled_tools) # aplica o deny-list
add_filter('rest_pre_dispatch', MCP_Host_Guard::guard, 5) # valida Host header
add_filter('rest_pre_dispatch'/'rest_post_dispatch', mcp_log_*) # log de pedidos MCP
↓
(Abilities API é lazy — só inicializa no primeiro wp_get_ability())
O PRÓPRIO adapter, ao arrancar (mcp_adapter_init prioridade 10), chama wp_get_ability()
internamente para descobrir tools → dispara wp_abilities_api_init → register_abilities() corre
ANTES do nosso register_mcp_server (prioridade 20) — por isso $this->ability_names já está
preenchido quando register_mcp_server lê $this->ability_names.
↓
mcp_adapter_init:20 → EMCP_Tools_Plugin::register_mcp_server($mcp_adapter)
→ $mcp_adapter->create_server('emcp-tools-server', 'mcp', 'emcp-tools-server', …, $tools, …)
→ endpoint fica disponível em /wp-json/mcp/emcp-tools-server
```
### 2.1 O preload "authoritative namespace" (`class-mcp-adapter-bootstrap.php`)
Problema real documentado no código (issue #99): quando **vários plugins activos** vendorizam
`wordpress/mcp-adapter` (ex. WooCommerce + EMCP Tools) em versões diferentes, e nem todos usam o
mesmo autoloader partilhado (Jetpack Autoloader), PHP carrega **classes individuais** de cópias
diferentes consoante qual plugin referencia cada classe primeiro — resultado: um `McpAdapter` da
v0.5.0 a par de um `HttpTransport` da v0.4.1 no mesmo request ("sheared namespace"), que falha
com `McpServerError: Session terminated` (JSON-RPC -32600).
**Mitigação do EMCP:** regista um autoloader `spl_autoload_register($cb, true, true)` (throw,
**prepend**) logo no arranque, servindo TODO o namespace `WP\MCP\` a partir da SUA cópia
vendorizada — antes que qualquer outro plugin possa referenciar uma classe do adapter. Reafirma-se
depois de carregar o Jetpack Autoloader do próprio adapter (que também se regista com prepend).
**Implicação para a réplica:** se formos "mais um plugin" a vendorizar `wordpress/mcp-adapter`
lado a lado com EMCP Tools/WooCommerce no mesmo WordPress, herdamos este mesmo risco de colisão.
Duas opções limpas: (a) usar o Jetpack Autoloader nós também (para participar na arbitragem de
versão-mais-alta em vez de competir com autoloaders simples), ou (b) não vendorizar — depender de
uma cópia `wordpress/mcp-adapter` partilhada ao nível do site (plugin dedicado só ao adapter,
todos os outros plugins declaram a dependência) — mais correcto a prazo mas exige coordenação com
todos os plugins que hoje bundlam a sua própria cópia.
## 3. `EMCP_Tools_Plugin` — orquestrador singleton
Ficheiro: `includes/class-plugin.php`. Contrato mínimo para a réplica:
- **`register_category()`** — `wp_register_ability_category('<prefixo>', ['label'=>…,
'description'=>…])`, chamado em `wp_abilities_api_categories_init`.
- **`register_abilities()`** — `$this->ability_names = $registrar->register_all($elementor_active)`,
chamado em `wp_abilities_api_init`. `$elementor_active` vem de
`EMCP_Tools_Bootstrap::elementor_active()` (guarda de dependência — grupos Elementor-dependentes
ficam todos atrás desta flag, ver doc 01/02 e a lista completa em `class-ability-registrar.php`
reproduzida no doc 10).
- **`get_active_ability_names()`** — força a inicialização lazy da Abilities API
(`wp_get_ability('emcp-tools/list-pages')` como gatilho) e devolve o array já filtrado pelo
deny-list; usado pelo dispatcher (compact mode) e por qualquer superfície externa (ex. um chat
de admin) que precise de saber "o que está mesmo disponível agora".
- **`filter_disabled_tools($names)`** — `array_values(array_diff($names,
get_option('emcp_tools_disabled_tools', [])))`, hook em `emcp_tools_ability_names`. **Este é o
único ponto de aplicação do deny-list** — mecanismo do deny-list incremental (34 migrações) já
documentado em `skill://emcp-tools` §8; aqui confirma-se o hook exacto que o aplica.
- **`register_mcp_server($mcp_adapter)`** — chamado em `mcp_adapter_init` prioridade 20 (depois da
Abilities API já ter corrido). Comportamento condicional:
- **Server gate** (`emcp_tools_server_enabled`, on por omissão): se off, abilities continuam
registadas no core mas **nenhum endpoint MCP é criado** — kill-switch total sem desregistar
nada.
- **Compact tool mode** (`emcp_tools_dispatcher_mode`, off por omissão): quando ligado, o
`$tools` passado a `create_server()` é só os 3 nomes do dispatcher
(`emcp-tools/{list-tools,get-tool-schema,call-tool}`) — o resto continua **registado e
invocável via `call-tool`**, só não aparece em `tools/list`. Quando desligado, `$tools` é a
lista cheia (+ as 3 abilities de contexto do core: `core/get-site-info`,
`core/get-user-info`, `core/get-environment-info`, sempre incluídas quando existirem).
- `create_server()` recebe: `server_id='emcp-tools-server'`, `route_namespace='mcp'`,
`route='emcp-tools-server'` (→ endpoint final `/wp-json/mcp/emcp-tools-server`), nome/descrição
(descrição compõe um resumo do ambiente do site via `EMCP_Tools_Site_Context`), versão,
`transports=[HttpTransport::class]`, `tools=$tools`, `resources=[]`, `prompts=[]`, e um
`transport_permission_callback` opcional: se o módulo OAuth (`EMCP_Tools_OAuth_Server`) está
activo, usa Bearer OAuth; senão cai no default do adapter (Application Password / cookie
admin).
## 4. Compact tool mode — o dispatcher de 3 tools
`includes/abilities/class-dispatcher-abilities.php` (ver excerto completo lido nesta sessão).
Sempre **registado** (para `wp_get_ability()` resolver os 3 nomes), mas só **exposto no servidor**
quando `emcp_tools_dispatcher_mode=1` (§3). Este é exactamente o padrão que os 3 servidores
`emcp-tools`/`emcp-emanuelalmeida`/`emcp-descomplicar` deste ecossistema já usam pelo lado do
cliente (system prompt: "Compact tool mode... Discover tools with list-tools, fetch inputs with
get-tool-schema, run with call-tool").
| Tool | O que faz | Enforcement |
|---|---|---|
| `list-tools` | Devolve `{name, description, category, destructive}` para cada ability activa (pós-deny-list), filtrável por `search`/`category`. Inclui `context` (resumo do ambiente via `Site_Context`). | `permission_callback`: `current_user_can('edit_posts')` — é só metadata/routing |
| `get-tool-schema` | Devolve `{description, inputSchema}` por nome, em lote (`names: string[]`). Nomes fora do set activo entram em `unavailable`. | idem |
| `call-tool` | Resolve `wp_get_ability($name)`, corre `check_permissions($args)` **da ability alvo** (nunca do dispatcher), depois `validate_input()` se existir, depois `execute($args)`. | **A gate real é sempre a da tool alvo** — o dispatcher nunca contorna `permission_callback` individual |
**Blueprint para a réplica:** replicar este padrão exactamente — um dispatcher de 3 tools que só
resolve/chama `wp_get_ability()`, nunca duplica lógica de permissão. É ~250 linhas triviais de
reescrever do zero (schema simples, sem estado) — não vale a pena tentar simplificar mais.
## 5. `emcp_tools_register_ability()` — o único ponto de entrada de registo
`includes/class-schema-compat.php`. Toda classe de abilities chama esta função global (nunca
`wp_register_ability()` directamente). Faz 3 coisas antes de delegar:
1. **`sanitize()`** — normaliza o JSON Schema para clientes exigentes: remove valores vazios de
`enum`, força `properties: {}` (objecto, nunca array vazio) — Gemini/Antigravity rejeitam a
forma "errada". Recursivo em `items`/`allOf`/`oneOf`/`anyOf`.
2. **`strictify()`** *(opt-in, `emcp_tools_strict_schemas`, off por omissão)* — reescreve o schema
para o modo "strict function calling" da OpenAI: toda a propriedade em `required`, as
originalmente opcionais tornam-se nullable, `additionalProperties: false` em objectos com
propriedades declaradas. Existe porque CrewAI e stacks OpenAI-compatíveis exigem esta forma; um
schema "normal" (opcionais fora de `required`) é rejeitado por eles.
3. **`wrap_execute_callback()`** — envolve o callback da ability em dois cuidados:
- **Veto de escrita** (`emcp_tools_before_write` filter, só para abilities não-read-only):
um listener pode devolver `WP_Error` para bloquear a escrita antes de correr — mecanismo real
de enforcement de guardrails (usado pela feature Pro "Memory Enforcer"). Seam útil para nós:
dá um ponto único onde qualquer política de governança (ex. CARL) pode vetar uma escrita MCP
sem tocar em cada ability individualmente.
- **`normalize_result()`** — garante que o retorno cabe em `structuredContent` do MCP (que o
schema tipa como objecto `{[key:string]:unknown}`, nunca lista). Arrays associativos e
objectos passam直; listas/escalares/`null` são embrulhados em `{data: …}`; `array()` vazio
TAMBÉM é embrulhado (PHP não distingue lista vazia de mapa vazio — desembrulhado serializa
para `[]`, forma inválida). **Bug real que isto evita:** a integração WooCommerce devolve
`WP_REST_Response::get_data()` verbatim, e várias rotas `wc/v3` respondem com array de topo
(ex. `report-products-totals`) — sem este wrapper, clientes MCP estritos rejeitam a resposta.
**Blueprint para a réplica:** replicar exactamente este envelope (sanitize + normalize_result no
mínimo; strictify só se formos alvo de clientes OpenAI-strict). É a peça que evita uma classe
inteira de bugs "funciona no Claude, parte no CrewAI" — não pular esta camada achando-a
acessória.
## 6. Guardas de infraestrutura (fora do fluxo funcional, mas obrigatórias)
- **`EMCP_Tools_MCP_Host_Guard::guard`** (`rest_pre_dispatch`, prioridade 5) — recusa pedidos MCP
cujo header `Host` já não bate com o `home` do site (conector apontado para um domínio
antigo/temporário). `no_store_headers` (`rest_pre_serve_request`) força `Cache-Control:
no-store` nas respostas MCP — proxies como LiteSpeed/QUIC descartam respostas suspeitas de cache
sem isto (issue nomeada no código).
- **`EMCP_Tools_MCP_Request_Log`** — grava tool/status/duração/`x-request-id` de cada pedido MCP
via `rest_pre_dispatch`(6)/`rest_post_dispatch`(10), só para rotas que começam por
`/mcp/emcp-tools-server` — alimenta a tab "MCP Log" do admin.
## 7. Guarda Free⇄Pro (single-instance)
`emcp-tools.php`, topo do ficheiro (antes de qualquer `require`). Free e Pro são **o mesmo código**
em duas pastas de plugin (`emcp-tools/` vs `emcp-pro/`), distinguidas só por um marcador
`.emcp-pro` no directório. Como WordPress trata as duas pastas como plugins distintos,
`require_once` não deduplica entre elas — activar ambas redeclara todas as classes → fatal error.
A guarda corre **antes de qualquer require**, detecta se já correu neste request
(`defined('EMCP_TOOLS_VERSION')`) e decide: Premium sempre ganha — a cópia Free cede (mostra aviso,
`return` antes de declarar nada) se a Premium estiver activa; a cópia Premium desactiva a Free se
esta já tiver arrancado primeiro. **Não relevante para a réplica** (é housekeeping específico do
modelo de distribuição Free/Pro deste vendor) — mas explica porque é seguro assumir que só UMA
"visão" do código corre por request, mesmo no build Free.
## 8. Superfície total (dimensionamento)
Ver `class-ability-registrar.php::register_groups()` para a lista EXACTA de classes/condições —
reproduzida integralmente no doc 10 (módulos) como referência cruzada, porque a ordem de registo
ali É a fonte de verdade de que grupo depende de que módulo/plugin/licença. Resumo por doc:
| Doc | Assunto | Nº aprox. de abilities |
|---|---|---|
| 01 | Elementor clássico (páginas, layout, widgets, templates, globals, custom code, composite) | ~35 |
| 02 | Elementor Atomic v4 + Gutenberg | ~25 |
| 03 | WordPress core (conteúdo, media, settings, plugins, temas, users, menus) + temas/frameworks | ~30 |
| 04 | Themer (CPT header/footer/single/archive/search/404 + Themer PHP) | ~13 |
| 05 | Redirects + Search + Snapshot + Change-ledger + Content-mirror | ~14 |
| 06 | Sandbox (PHP snippets, custom widgets/blocks, cloud bundle) | ~20 |
| 07 | Filesystem + DB + WP-CLI + Security + Performance | ~15 |
| 08 | Integrações terceiros (ACF, Meta Box, forms, SEO) | ~30 |
| 09 | Stock images + Cloud sync + OAuth | ~17 |
| 10 | Módulos (lifecycle) + inventário Pro-only (metadata) | n/a (estrutural) |
Total bate aproximadamente com os 162-165 confirmados ao vivo em `skill://emcp-tools` §7 — a
diferença entre sites reflecte quais integrações de terceiros estão de facto instaladas.
## Fonte
Leitura directa (19-08-2026) de: `emcp-tools.php`, `includes/class-mcp-adapter-bootstrap.php`,
`includes/class-plugin.php`, `includes/class-schema-compat.php`,
`includes/abilities/class-ability-registrar.php`,
`includes/abilities/class-dispatcher-abilities.php`,
`vendor/wordpress/mcp-adapter/composer.json`, `vendor/wordpress/php-mcp-schema/composer.json`;
inventário completo de ficheiros (`find … -name '*.php' -exec wc -l`, 128 465 linhas). Cruzado com
`skill://emcp-tools` (auditoria de postura de segurança, 16-08-2026) para os números de abilities
activas/desligadas por site.