# 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('', ['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.