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).
20 KiB
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 emwp_abilities_api_categories_init.register_abilities()—$this->ability_names = $registrar->register_all($elementor_active), chamado emwp_abilities_api_init.$elementor_activevem deEMCP_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 emclass-ability-registrar.phpreproduzida 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 ememcp_tools_ability_names. Este é o único ponto de aplicação do deny-list — mecanismo do deny-list incremental (34 migrações) já documentado emskill://emcp-tools§8; aqui confirma-se o hook exacto que o aplica.register_mcp_server($mcp_adapter)— chamado emmcp_adapter_initprioridade 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$toolspassado acreate_server()é só os 3 nomes do dispatcher (emcp-tools/{list-tools,get-tool-schema,call-tool}) — o resto continua registado e invocável viacall-tool, só não aparece emtools/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 viaEMCP_Tools_Site_Context), versão,transports=[HttpTransport::class],tools=$tools,resources=[],prompts=[], e umtransport_permission_callbackopcional: 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).
- Server gate (
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:
sanitize()— normaliza o JSON Schema para clientes exigentes: remove valores vazios deenum, forçaproperties: {}(objecto, nunca array vazio) — Gemini/Antigravity rejeitam a forma "errada". Recursivo emitems/allOf/oneOf/anyOf.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 emrequired, as originalmente opcionais tornam-se nullable,additionalProperties: falseem objectos com propriedades declaradas. Existe porque CrewAI e stacks OpenAI-compatíveis exigem esta forma; um schema "normal" (opcionais fora derequired) é rejeitado por eles.wrap_execute_callback()— envolve o callback da ability em dois cuidados:- Veto de escrita (
emcp_tools_before_writefilter, só para abilities não-read-only): um listener pode devolverWP_Errorpara 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 emstructuredContentdo MCP (que o schema tipa como objecto{[key:string]:unknown}, nunca lista). Arrays associativos e objectos passam直; listas/escalares/nullsã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 devolveWP_REST_Response::get_data()verbatim, e várias rotaswc/v3respondem com array de topo (ex.report-products-totals) — sem este wrapper, clientes MCP estritos rejeitam a resposta.
- Veto de escrita (
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 headerHostjá não bate com ohomedo site (conector apontado para um domínio antigo/temporário).no_store_headers(rest_pre_serve_request) forçaCache-Control: no-storenas 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-idde cada pedido MCP viarest_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.