# 13 — Forms Pro: blueprint de abilities MCP (WPForms + Fluent Forms) Fonte primária: leitura directa (19-08-2026) do **WPForms Lite 2.0.0.5** e do **Fluent Forms (free) 6.2.12**, ambos instalados e **activos** em `ecommerce-demo.descomplicar.pt` (`/home/ealmeida/ecommerce-demo.descomplicar.pt/wp-content/plugins/{wpforms-lite,fluentform}/`), confirmado por `wp plugin list --format=json` antes de qualquer leitura de código (secção 6). Cruzado com `docs/08-INTEGRACOES-TERCEIROS.md` §3-§4 (a classe base **real** e **Free** `EMCP_Tools_Form_Integration` + a integração CF7 concreta, únicas peças do domínio "Forms" que existem no código EMCP Tools instalado) e `docs/10-MODULOS-E-INVENTARIO-PRO.md` linhas 470-476 (confirmação do gate `EMCP_Tools_WPForms_Integration`/`EMCP_Tools_FluentForms_Integration` — ambas Pro-only, ficheiro ausente desta árvore). **Exclui por completo** SEO/Yoast/Rank Math/WooCommerce, fora de âmbito. ## 0. Panorama — porque este documento é diferente dos outros 12 Este documento tem a mesma forma que o 11 (WooCommerce) e o 12 (Widget Builder): a peça que faltaria auditar — `EMCP_Tools_WPForms_Integration`/`EMCP_Tools_FluentForms_Integration` — está **fisicamente ausente** desta árvore Free (confirmado em `docs/08` §7 e `docs/10` linhas 470-476: referenciadas por `class_exists()` no `class-ability-registrar.php`, nunca `require_once`). O que muda aqui é a base sobre a qual se propõe o desenho: existe, **e é código real, Free, já lido na íntegra**, uma classe abstracta partilhada `EMCP_Tools_Form_Integration` (`includes/abilities/forms/class-form-integration.php`, doc08 §3) da qual **uma** integração concreta real herda — `EMCP_Tools_CF7_Integration` (doc08 §4). WPForms e Fluent Forms seriam, numa implementação real do EMCP Pro, mais duas classes que estendem a MESMA base — não um desenho paralelo. Este documento não reinventa o dispatcher; reaproveita o contrato já confirmado no doc08 e preenche as duas células concretas que faltam. Três fontes fundamentam cada afirmação abaixo, e cada tabela di-lo explicitamente: 1. **A classe base + CF7, código real Free já lido** (`docs/08-INTEGRACOES-TERCEIROS.md` §3-§4) — o contrato `id()/label()/is_active()/operations()`, o dispatcher `dispatch()`, a gate de confirmação genérica (`arguments.confirm===true`), as capabilities coarse (`can_read()= edit_posts`, `can_write()=manage_options`), e a nota de que **`destructive=true` é o default do tool `write` desta base específica** (diferente de ACF/Meta Box/SEO) — citado tal-qual do doc08. 2. **O catálogo de 4 tools/11 operações da captura de ecrã** partilhada pelo utilizador — usado **apenas** como especificação da API-alvo (nomes de tool, descrições, lista de operações), nunca como fonte de schema ou de comportamento interno. Citado abaixo tal-qual foi fornecido, com a marcação `[CAPTURA DE ECRÃ]`. 3. **Leitura directa do código real de WPForms Lite e Fluent Forms** — os dois plugins que a captura nomeia, ambos confirmados **instalados e activos** no mesmo site do bundle Descomplicar (`ecommerce-demo.descomplicar.pt`), e lidos ao nível dos métodos concretos que uma implementação real teria de chamar. Secções 1 e 2. > **Convenção**: `[REAL]` = confirmado por leitura de código (EMCP Free, WPForms Lite ou Fluent > Forms). `[CAPTURA DE ECRÃ]` = vem apenas da imagem partilhada pelo utilizador (nome do tool, > descrição, lista de operações) — usada como especificação-alvo, não como fonte de comportamento. > `[DESENHO PRÓPRIO]` = proposta nossa sem equivalente confirmado em nenhuma das duas fontes > anteriores. **A descoberta mais importante deste documento**: ao ler o código de Fluent Forms para fundamentar a tabela de ability proposta, descobriu-se que a versão instalada (6.2.12) **já embarca o seu próprio servidor MCP nativo** — `FluentForm\App\Modules\MCP\AbilitiesRegistrar`, registado via `wp_register_ability()` (a mesma API core que o EMCP Tools usa), com 9 abilities reais, um padrão dry-run+confirm-token mais rigoroso do que qualquer coisa documentada nesta série até agora, e um modelo de permissões (`Acl`) próprio. Isto **não é o EMCP Tools** — é uma superfície MCP completamente independente e paralela, que o próprio Fluent Forms passou a incluir. A secção 2.3 documenta-a na íntegra porque muda a recomendação de blueprint: uma integração EMCP real para Fluent Forms poderia **detectar e delegar** nesta camada nativa em vez de reimplementar CRUD do zero — algo que não se aplica a nenhuma das outras integrações de terceiros desta série. **WPForms Lite não tem nada equivalente** (confirmado por ausência de qualquer pasta/classe MCP na árvore `src/` do plugin, secção 1). **Segunda distinção importante, também confirmada por leitura de código**: existem **dois eixos free/pro independentes** em jogo aqui, não um. O eixo 1 é o do EMCP Tools em si (Free vs Pro) — já documentado em doc08/doc10, é o que decide se `wpforms-read`/`wpforms-write` existem de todo como abilities MCP. O eixo 2 é **interno a cada plugin de formulários** — WPForms Lite vs WPForms Pro — e é **completamente ortogonal** ao primeiro: mesmo com o EMCP Tools Pro totalmente licenciado, a operação `list-entries` de `wpforms-read` nunca teria dados para devolver contra uma instalação WPForms **Lite**, porque a Lite genuinely não guarda submissões (secção 1.4). A frase da captura de ecrã — *"entries require WPForms Pro"* — refere-se exactamente a este segundo eixo, e a secção 1.4 confirma-a byte a byte no código lido. --- ## 1. WPForms (Lite 2.0.0.5) ### 1.1 Ficheiros lidos `includes/class-form.php` (`WPForms_Form_Handler`, integral), `src/WPForms.php` (bootstrap + registry `obj()`, integral), `src/Access/Capabilities.php` (integral), `includes/functions/access.php` (integral), `lite/wpforms-lite.php` (integral — bootstrap específico da build Lite, incl. o painel de Entries com dados de amostra). ### 1.2 O contrato de dados real: `WPForms_Form_Handler` `[REAL]` Registado em `wpforms()->obj('form')` (só `'form'` e `'process'` são registados no `objects()` da classe principal — ver 1.4). Um "formulário" WPForms é um `WP_Post` do CPT `wpforms` (ou `wpforms-template`), com o formulário inteiro (campos, definições, notificações, confirmações) serializado em `post_content` (JSON via `wpforms_encode()`/`wpforms_decode()`), não em postmeta individual. | Método | O que faz | Nota | |---|---|---| | `get( $id = '', $args = [] )` | Sem `$id`: `get_multiple()` (lista, `WP_Query`-like via `get_posts()`, aceita `search`/`post_status`/etc.). Com `$id`: `get_single()` — devolve `WP_Post` ou, com `content_only:true`, o array JSON decodificado | Filtro de gate: se `$args['cap']` fornecida (ou detectada automaticamente via `access_obj->init_allowed()`), chama `wpforms_current_user_can($cap, $id)` antes de devolver | | `add( $title, $args, $data )` | Cria um formulário novo (post + `post_content` inicial); requer capability `create_forms` | Se criado fora do form builder (`data.builder===false`), popula notificação/confirmação por omissão automaticamente | | `update( $form_id, $data, $args )` | Substitui o `post_content` inteiro por `wpforms_encode($data)`, preservando `meta` e `fields[].meta` do estado anterior (`get_meta()`/`update__preserve_fields_meta()`) | Capability por omissão: `edit_form_single` | | `delete( $ids )` | Apaga o post **definitivamente** (`wp_delete_post($id, true)` — sem trash), e em cascata `entry`/`entry_meta`/`entry_fields` **se esses objectos existirem no registry** (nunca existem em Lite — ver 1.4) | Capability: `delete_form_single` | | `duplicate( $ids )` | Clona formulário(s), renomeia com sufixo `(ID #N)`, injecta avisos de "Zap desligado" se aplicável | Capability: `create_forms` para criar + `view_form_single` para ler o original | | `update_status( $form_id, $status )` | Só aceita `publish`/`trash` (filtrável); é o mecanismo real de "apagar suavemente" um formulário | **Nota do código**: usa a capability `delete_form_single`, não `edit_form_single`, mesmo para restaurar — comentário explícito: "trash and restore are actually part of the deletion operation" | | `get_meta`/`update_meta`/`delete_meta` | Ler/escrever/apagar uma chave dentro do bloco `meta` do formulário (usado para `category`/`subcategory`, entre outros) | Capability por omissão: `edit_form_single` | | `next_field_id`, `get_field`, `get_field_meta` | Utilitários de builder — atribuição do próximo ID de campo, leitura de um campo/a sua meta | — | ### 1.3 O colapso de capabilities em Lite: tudo vira `manage_options` `[REAL]` Citação directa e integral de `src/Access/Capabilities.php` (a classe real por trás de `wpforms()->obj('access')`, que `wpforms_current_user_can()` chama): ```php public function current_user_can( $caps = [], $id = 0 ) { return \current_user_can( \wpforms_get_capability_manage_options() ); } ``` `wpforms_get_capability_manage_options()` devolve `'manage_options'` (filtrável, mas sem filtro aplicado nesta instalação). **Independentemente do argumento `$caps` passado** — `create_forms`, `edit_form_single`, `delete_form_single`, `view_form_single`, `view_own_forms`, `view_others_forms`, etc. — o resultado é sempre o mesmo: `current_user_can('manage_options')`. Esta é a implementação de `Access Controls` no Lite; a versão Pro do WPForms substitui esta classe por uma com granularidade real por role (é uma feature Pro do **WPForms em si**, não do EMCP). **Implicação directa para o desenho da ability**: ao contrário do padrão documentado em `docs/08` §0 para ACF/Meta Box/Forms ("gate coarse larga no MCP + gate fina por operação dentro do `dispatch()`"), contra uma instalação **WPForms Lite** a gate fina É a gate coarse — não há separação real de privilégio possível entre `list-forms` (deveria ser `edit_posts`-like) e `delete-form` (deveria exigir mais) enquanto o alvo for Lite. Uma implementação honesta deveria expor este facto ao agente (ex.: um campo `effective_gate: 'manage_options (WPForms Lite has no granular Access Controls)'` na resposta de descoberta), em vez de fingir que os `perm` por operação da tabela 1.6/1.7 têm efeito real nesta instalação concreta. ### 1.4 "Entries": ausência real confirmada, não apenas mencionada na descrição `[REAL]` Duas confirmações independentes, ambas por leitura directa de código, provam que WPForms Lite **não tem** armazenamento de submissões — não é uma limitação de licença que a API esconda, é código genuinamente ausente: 1. **O registry nunca regista `'entry'`.** `src/WPForms.php::objects()` (chamado em `plugins_loaded`): ```php public function objects(): void { $this->registry['form'] = new WPForms_Form_Handler(); $this->registry['process'] = new WPForms_Process(); do_action( 'wpforms_loaded' ); } ``` Apenas `form` e `process`. `obj( 'entry' )`/`obj( 'entry_meta' )`/`obj( 'entry_fields' )` devolvem sempre `null` (`return $this->registry[$name] ?? null;`) — o próprio `WPForms_Form_Handler::delete()` já lida com isto defensivamente: ```php $entry_obj = wpforms()->obj( 'entry' ); $entry_meta_obj = wpforms()->obj( 'entry_meta' ); $entry_fields_obj = wpforms()->obj( 'entry_fields' ); if ( $entry_obj && $entry_meta_obj && $entry_fields_obj ) { /* delete entries */ } ``` Em Lite este `if` é sempre falso — não há nada para apagar porque não há classe registada. 2. **A página admin "Entries" mostra dados FALSOS, codificados no plugin.** `lite/wpforms-lite.php`, método `get_entries_list_data()`, devolve um array PHP fixo (`'Michael Johnson'`, `'David Thompson'`, `'Sarah Parker'`, …) — 12 nomes hard-coded, nenhum vindo de base de dados — apresentado com um CTA de upgrade. O comentário da UI é explícito: *"Display sample data and notify user that entries is a pro feature."* **Conclusão directa para a ability proposta**: as operações `list-entries`/`get-entry` (secção 1.6) e `update-entry-status`/`delete-entry` (secção 1.7) da captura de ecrã **não têm nenhuma superfície de código a auditar nesta instalação Lite** — a única grounding possível é negativa (confirmar que genuinamente não existem), o que casa exactamente com o parêntesis da própria descrição capturada: *"entries require WPForms Pro"*. ### 1.5 Estrutura real de notificações e confirmações `[REAL]` De `lite/wpforms-lite.php::form_settings_notifications()`/`form_settings_confirmations()` — os dois blocos do formulário guardados em `post_content.settings`: | Bloco | Chaves reais confirmadas | Nota | |---|---|---| | `notifications` (array indexado por ID, `'1'` = "Default Notification") | `email` (destinatário; suporta lista separada por vírgulas e smart tags `{admin_email}`/`{user_email}`), `carboncopy` (CC — só visível se a definição global `email-carbon-copy` estiver activa), `subject`, `sender_name`, `sender_address`, `replyto`, `message` (default `{all_fields}`) | Em Lite só a notificação `1` é editável na UI ("Add New Notification" é um `education-modal` — upsell); a estrutura de dados em si já suporta múltiplas | | `confirmations` (array indexado por ID, `'1'` = "Default Confirmation") | `type` (`message`\|`page`\|`redirect`), `message`, `message_scroll` (bool), `page` (post ID), `page_url_parameters`, `redirect` (URL), `redirect_new_tab` (bool) | Mesma limitação de UI que notifications em Lite | ### 1.6 Tabela de ability proposta — `emcp-tools/wpforms-read` **Descrição alvo `[CAPTURA DE ECRÃ]`**: "Read WPForms forms, fields, notifications, and entries (entries require WPForms Pro)". PRO. READ-ONLY. 6 operações. | operação `[CAPTURA DE ECRÃ]` | Fundamentação `[REAL]` | Args propostos | Nível de risco | |---|---|---|---| | `list-forms` | `WPForms_Form_Handler::get( '', $args )` → `get_multiple()`; título/estado/contagem de campos derivados de `post_title`/`post_status`/`content_only` decode | `{ search?, status?, page?, per_page? }` | readonly | | `get-form` | `get( $form_id, ['content_only'=>true] )` → devolve o array JSON completo: `fields[]`, `settings` (incl. `notifications`, `confirmations`), `meta` | `{ form_id }` | readonly | | `list-notifications` | Subconjunto `settings.notifications` do form completo (secção 1.5) | `{ form_id }` | readonly | | `get-settings` | Subconjunto `settings.confirmations` + campos gerais não-notificação de `settings` (`form_title`, `form_desc`, `submit_text`, etc.) | `{ form_id }` | readonly | | `list-entries` | **Sem grounding possível nesta instalação** — `wpforms()->obj('entry')` é `null` em Lite (secção 1.4); a operação só teria dados numa instalação **WPForms Pro** real (código não presente em nenhum lado desta série) | `{ form_id, status?, page?, per_page? }` | readonly — **mas sempre devolveria `plugin_inactive`/equivalente contra Lite**, não uma lista vazia disfarçada de sucesso | | `get-entry` | Idem — sem grounding; depende de `WPForms_Entry_Handler` (Pro), classe ausente de toda a árvore auditada nesta série | `{ entry_id }` | readonly — mesma nota | ### 1.7 Tabela de ability proposta — `emcp-tools/wpforms-write` **Descrição alvo `[CAPTURA DE ECRÃ]`**: "Update WPForms notifications, set entry status, and delete entries (confirm:true). Entry operations require WPForms Pro." PRO. DESTRUCTIVE. 3 operações. | operação `[CAPTURA DE ECRÃ]` | Fundamentação `[REAL]` | Args propostos | Nível de risco | |---|---|---|---| | `update-notification` | `WPForms_Form_Handler::update( $form_id, $data )` com `$data['settings']['notifications'][$id]` alterado — a chamada real substitui o `post_content` inteiro, por isso uma implementação correcta tem de fazer merge no lado do EMCP: ler o form completo, alterar só a notificação alvo, escrever de volta | `{ form_id, notification_id, email?, carboncopy?, subject?, sender_name?, sender_address?, replyto?, message? }` | write, não-destructive por si (mas o tool inteiro herda `destructive=true` da base, secção 3) | | `update-entry-status` | Sem grounding — depende de `entry`/`entry_meta` (Pro, ausentes) | `{ entry_id, status }` | write — sempre `plugin_inactive` contra Lite | | `delete-entry` | Sem grounding — mesma ausência; a base `EMCP_Tools_Form_Integration::dispatch()` (doc08 §3.2) já suporta nativamente a gate `confirm:true` que a descrição da captura exige ("confirm:true") — esse mecanismo **já existe e é real**, mesmo que os dados que apagaria não existam em Lite | `{ entry_id, confirm: true }` | **destructive** — irreversível numa instalação Pro real | --- ## 2. Fluent Forms (free 6.2.12) ### 2.1 Ficheiros lidos `app/Models/Form.php`, `app/Models/Submission.php` (ambos integrais — ORM `FluentForm\Framework`, tabelas `fluentform_forms`/`fluentform_submissions`), `app/Api/Form.php`, `app/Api/Entry.php` (integrais — a fachada pública de leitura usada por shortcodes/relatórios internos), `app/Http/Policies/FormPolicy.php`, `app/Http/Policies/SubmissionPolicy.php` (integrais), `app/Http/Controllers/McpSettingsController.php` (integral), `app/Modules/MCP/ AbilitiesRegistrar.php` (integral), `app/Modules/MCP/Tools/FormTools.php`, `app/Modules/MCP/Tools/SubmissionTools.php` (integrais), `app/Modules/MCP/Support/ PermissionGate.php`, `app/Modules/MCP/Support/WriteGuard.php` (integrais), `app/Modules/Acl/Acl.php` (integral). ### 2.2 O contrato de dados real: `Form`/`Submission` (ORM `FluentForm\Framework`) `[REAL]` Ao contrário do WPForms (post type + JSON em `post_content`), Fluent Forms usa **tabelas próprias do plugin**: `fluentform_forms` (colunas confirmadas via docblock `@property`: `id, title, status, appearance_settings, form_fields, has_payment, type, conditions, created_by, created_at, updated_at`) e `fluentform_submissions` (`id, form_id, serial_number, response, source_url, user_id, status, is_favourite, browser, device, ip, city, country, payment_status, payment_method, payment_type, currency, payment_total, total_paid, created_at, updated_at`). `status` do formulário: `published`/`unpublished` (confirmado em `Form::prepare()`, default `'published'`). `status` da submissão: `unread`/`read`/`spam`/`trashed` (confirmado directamente na `SubmissionTools` nativa — secção 2.3 — e em `Submission::countByGroup()`); `is_favourite` é um campo **separado** (boolean), não um valor de `status`. `Form::remove($formId)` (estático) e `Submission::remove($submissionIds, $formId)` (estático, agora **exige** `$formId` — `throw new \InvalidArgumentException` se omitido, um "fail-closed scope guard" explícito no código) fazem cascata real de apagamento: meta, entry details, analytics, logs, e (se pagamentos estiverem activos) order items/transactions/subscriptions. ### 2.3 A descoberta central: Fluent Forms já embarca o seu próprio servidor MCP nativo `[REAL]` Não é uma integração de terceiros comum — é uma **superfície MCP paralela e independente do EMCP Tools**, presente na versão 6.2.12 instalada, registada com a mesma API core WordPress (`wp_register_ability()`) que o próprio EMCP Tools usa. A classe orquestradora é `FluentForm\App\Modules\MCP\AbilitiesRegistrar` (citação directa do seu docblock): > "Single source of truth for every FluentForm MCP ability... Pro tools are NOT listed here — they > push abilities via the `fluentform/mcp_loaded` action + `fluentform/mcp_ability_names` filter." **Estado ligado/desligado**: a feature vem **desligada por omissão** — `PermissionGate::isEnabled()` lê a option dedicada `_fluentform_mcp_settings['enabled']==='yes'`, activável em FluentForm → Settings → MCP (`McpSettingsController::toggle()`, exige `manage_options`). Requer também um plugin adaptador MCP instalado (`fluent-toolkit` ou `mcp-adapter`) — nenhum dos dois está instalado nesta instalação, por isso este servidor MCP nativo não está actualmente a correr em `ecommerce-demo.descomplicar.pt`, mas o código das 9 abilities está completo e presente. **As 9 abilities reais registadas** (`FormTools` + `SubmissionTools`; há mais 6 classes `IntegrationTools`/`StylingTools`/`FieldTools`/`NotificationTools`/`ReportTools`/`ContextTools` não detalhadas aqui por estarem fora do âmbito "forms" directo desta tabela, mas seguem o mesmo padrão): | Ability (nome real) | mode | `capability` real | Descrição (citação exacta do código) | |---|---|---|---| | `fluentform/list-forms` | read | `['fluentform_forms_manager', 'fluentform_dashboard_access']` (qualquer uma) | "Find and filter forms with compact rows (id, title, status, type, entry count)." | | `fluentform/get-form` | read | idem | "Full detail for one form: status, type, timestamps, and the field schema." | | `fluentform/create-form` | write | `fluentform_forms_manager` | Cria formulário a partir de título + lista de campos tipados | | `fluentform/list-submissions` | read | `fluentform_entries_viewer` | "List and filter entries for one form (form_id required)." | | `fluentform/get-submission` | read | `fluentform_entries_viewer` | "Full detail for one entry by id... fields as label/value pairs." | | `fluentform/update-submission-status` | write | `fluentform_manage_entries` | `status` ∈ `{unread, read, spam, trashed}`; `trashed` é soft-delete reversível | | `fluentform/add-submission-note` | write | `fluentform_manage_entries` | Nota interna de staff, não visível ao submissor | | `fluentform/delete-submission` | write, **destructive** | `fluentform_manage_entries` | Apagamento **permanente**, irreversível | | `fluentform/bulk-update-submissions` | write, **destructive** | `fluentform_manage_entries` | Acção em lote (`read`\|`unread`\|`trashed`\|`favorite`\|`unfavorite`\|`delete_permanently`), máx. 200 IDs por chamada | **O mecanismo de segurança mais rigoroso documentado nesta série inteira**: TODAS as 6 operações de escrita (mesmo `add-submission-note`, que não parece destructive) passam por `Mutation::runGuarded()`, que impõe um round-trip **dry-run → `confirm_token` → execução** — citação directa do docblock de `WriteGuard`: > "EVERY write routes through `Mutation::runGuarded`, not just the destructive ones. The reason is > prompt injection: entry field values are written by anonymous members of the public and land in > the agent's context... The dry-run → confirm_token round-trip is the one mechanism here that > forces a second pass through the operator before anything is written, so an instruction smuggled > into a form submission cannot complete a write on its own." O token é vinculado a um **fingerprint do estado actual** da entidade (ex.: para `update-submission-status`, o `status` actual faz parte da chave — um token gerado para "aprovar mudar para `read`" nunca pode ser reaproveitado para `trashed`); expira em 300s (`CONFIRM_TTL`); há ainda uma chave de idempotência opcional (`idempotency_key`, TTL 86400s) para retries seguros, e um mecanismo de "claim" via `INSERT IGNORE` atómico em `wp_options` para serializar execuções concorrentes do mesmo `(tool, entidade)`. Todo o texto de campos submetidos por visitantes anónimos que chega ao agente é explicitamente delimitado com marcadores `[[UNTRUSTED_USER_INPUT]]` (`MCPHelper::untrusted()`) antes de sair de `get-submission`/ `list-submissions` — uma defesa directa contra prompt injection via conteúdo de formulário. **Nota de segurança honesta, citada do próprio código** (docblock de `PermissionGate`), sobre um limite real deste modelo: `Acl::hasPermission()` concede qualquer permissão FluentForm (excepto `fluentform_full_access`) a um utilizador cujo *role* WordPress tenha sido delegado via "Managers" a nível de role — o que significa que um Editor com acesso FluentForm delegado por role **passa a gate de capability em TODAS as ferramentas, incluindo `delete-submission`**, independentemente da capability específica que essa ferramenta declara. Só utilizadores com concessão **individual** (`Acl::attachPermissions()`, per-user) são de facto restringidos como as declarações da tabela acima sugerem. Citação directa: *"the real boundary is 'authenticated WordPress user with FluentForm access, restricted to their form scope' — NOT per-tool capability separation. Do not describe it as the latter."* ### 2.4 Capabilities reais / modelo `Acl` 8 permissões nativas (`Acl::getPermissionSet()`): `fluentform_dashboard_access`, `fluentform_forms_manager`, `fluentform_entries_viewer`, `fluentform_manage_entries`, `fluentform_view_payments`, `fluentform_manage_payments`, `fluentform_settings_manager`, `fluentform_full_access`. **Por omissão só a role `administrator` as recebe** — `Acl::setPermissions()` adiciona as 8 capabilities apenas a `get_role('administrator')`; qualquer outro utilizador/role precisa de concessão explícita via FluentForm → Settings → Managers. `manage_options` bypassa sempre tudo (`hasExplicitFullAccess()`). ### 2.5 Tabela de ability proposta — `emcp-tools/fluentforms-read` **Descrição alvo `[CAPTURA DE ECRÃ]`**: "Read Fluent Forms forms, fields, and submissions." PRO. READ-ONLY. 4 operações. | operação `[CAPTURA DE ECRÃ]` | Fundamentação `[REAL]` | Equivalente nativo directo | Args propostos | |---|---|---|---| | `list-forms` | `Form` model + `Api\Form::forms()` (fachada pública já existente, paginação/pesquisa/filtro por `status`/`filter_by` embutidos) | **Mapeia 1:1** para a ability nativa `fluentform/list-forms` (secção 2.3) | `{ search?, status?, page?, per_page? }` | | `get-form` | `Api\Form::find()`/`form()` + `FormFieldsParser`/`FormService::getInputsAndLabels()` para o schema de campos | **Mapeia 1:1** para `fluentform/get-form` | `{ form_id }` | | `list-entries` | `Api\Entry::entries()` (fachada pública) ou `Submission::customQuery()`/`paginateEntries()` directamente | **Mapeia 1:1** para `fluentform/list-submissions` — inclui o mesmo enum `status ∈ {unread, read, spam, trashed, favorites}` | `{ form_id, status?, search?, date_from?, date_to?, page?, per_page? }` | | `get-entry` | `Api\Entry::entry()`/`getFormattedEntry()` (label+valor por campo, resolve utilizador associado) | **Mapeia 1:1** para `fluentform/get-submission` | `{ entry_id }` | ### 2.6 Tabela de ability proposta — `emcp-tools/fluentforms-write` **Descrição alvo `[CAPTURA DE ECRÃ]`**: "Set Fluent Forms submission status and delete submissions (confirm:true)." PRO. DESTRUCTIVE. 2 operações. | operação `[CAPTURA DE ECRÃ]` | Fundamentação `[REAL]` | Equivalente nativo directo | Args propostos | |---|---|---|---| | `update-entry-status` | `Submission::amend($id, ['status'=>...])`, ou via a fachada `SubmissionService::updateStatus()` | **Mapeia 1:1** para `fluentform/update-submission-status` — mesmo enum `unread/read/spam/trashed`, mesma nota de que `trashed` é reversível | `{ entry_id, status, confirm: true }` | | `delete-entry` | `Submission::remove($submissionIds, $formId)` (estático; agora exige `$formId` explícito — "fail-closed scope guard", citação directa do código) — cascata para meta/entry-details/logs/pagamentos | **Mapeia 1:1** para `fluentform/delete-submission` (`annotations.destructive=true` na base MCP) | `{ entry_id, form_id, confirm: true }` | **Nota de design que a captura já reflecte correctamente**: a descrição alvo `(confirm:true)` para `fluentforms-write` casa exactamente com o mecanismo genérico já real na base `EMCP_Tools_Form_Integration::dispatch()` (`arguments.confirm===true` obrigatório quando `operations()[$op]['confirm']===true`, doc08 §3.2). O que o EMCP Tools faria de mais simples aqui (gate binária de confirmação) já existe hoje na própria Fluent Forms de forma **mais forte** (dry-run + token vinculado a fingerprint de estado + expiração + idempotência) — uma diferença de desenho relevante para a secção 5. ### 2.7 Assimetria WPForms vs Fluent Forms — confirmada, não presumida A árvore `src/` de WPForms Lite (listada na íntegra: `Access, Admin, Analytics, Db, Education, Emails, Forms, Frontend, Helpers, Integrations, Lite, Logger, Migrations, Providers, Requirements, SetupChecklist, SetupWizard, SmartTags, Tasks`, mais `API.php`/`ErrorHandler.php`/ `Loader.php`/`WPForms.php` de topo) **não contém nenhuma pasta ou classe MCP** — `src/API.php` é um registo interno de callbacks (import de formulários), sem qualquer relação com Model Context Protocol. Fluent Forms, pelo contrário, tem uma pasta dedicada `app/Modules/MCP/` com 9 abilities reais completas. Esta assimetria é directamente relevante para a secção 5 (prioridade de construção): uma integração EMCP para Fluent Forms tem uma superfície de referência real e testável para se alinhar (ou mesmo delegar); uma para WPForms tem de ser construída inteiramente de raiz sobre `WPForms_Form_Handler`. --- ## 3. Comparação com o padrão base real (`EMCP_Tools_Form_Integration::dispatch()`) Citação do contrato já confirmado em `docs/08-INTEGRACOES-TERCEIROS.md` §3.1-§3.2: cada integração concreta implementa `id()`, `label()`, `is_active()`, `operations(): array` — mapa `nome => { mode, run, perm, desc, confirm? }` — e a base fornece `register()` (regista os dois tools `-read`/`-write`), `can_read()=edit_posts`, `can_write()=manage_options`, e `dispatch()` (resolve `operation`, verifica `is_active()`, valida existência+modo, corre `perm`, aplica a gate de confirmação genérica se `confirm===true`). Aplicando isto às duas famílias deste documento: ```php // Esqueleto de EMCP_Tools_WPForms_Integration extends EMCP_Tools_Form_Integration public function id(): string { return 'wpforms'; } public function label(): string { return 'WPForms'; } public function is_active(): bool { return class_exists( 'WPForms_Form_Handler' ) || defined( 'WPFORMS_VERSION' ); } public function operations(): array { return [ 'list-forms' => [ 'mode' => 'read', 'run' => [ $this, 'op_list_forms' ], 'perm' => 'edit_posts' ], 'get-form' => [ 'mode' => 'read', 'run' => [ $this, 'op_get_form' ], 'perm' => 'edit_posts' ], 'list-notifications' => [ 'mode' => 'read', 'run' => [ $this, 'op_list_notif' ], 'perm' => 'edit_posts' ], 'get-settings' => [ 'mode' => 'read', 'run' => [ $this, 'op_get_settings' ], 'perm' => 'edit_posts' ], 'list-entries' => [ 'mode' => 'read', 'run' => [ $this, 'op_list_entries' ], 'perm' => 'edit_posts' ], // op_list_entries devolve plugin_inactive-like se wpforms()->is_pro()===false 'get-entry' => [ 'mode' => 'read', 'run' => [ $this, 'op_get_entry' ], 'perm' => 'edit_posts' ], 'update-notification' => [ 'mode' => 'write', 'run' => [ $this, 'op_update_notif' ], 'perm' => 'manage_options' ], 'update-entry-status' => [ 'mode' => 'write', 'run' => [ $this, 'op_update_status' ], 'perm' => 'manage_options', 'confirm' => true ], 'delete-entry' => [ 'mode' => 'write', 'run' => [ $this, 'op_delete_entry' ], 'perm' => 'manage_options', 'confirm' => true ], ]; } ``` A gate `confirm=>true` mapeia directamente para o `(confirm:true)` que a própria descrição da captura de ecrã já anuncia para `wpforms-write` — sem inventar mecanismo novo, o que já existe na base serve tal-qual. O mesmo esqueleto para `EMCP_Tools_FluentForms_Integration` seria idêntico na forma, mas cada `run` callback delegaria para os métodos estáticos já reais de `FluentForm\App\Api\Form`/`Entry` (secção 2.2) — **ou**, mais interessante dado o achado da secção 2.3, para os métodos estáticos das próprias classes `FormTools`/`SubmissionTools` nativas do Fluent Forms, se detectadas presentes (ver secção 5). **Nota sobre `destructive` a nível de tool**: a base Forms marca o tool `write` inteiro como `destructive=true` por omissão (doc08 §3.2, diferente de ACF/Meta Box/SEO) — precisamente porque, ao contrário de CF7 (a única integração Free, que nunca apaga nada), tanto `wpforms-write` como `fluentforms-write` têm pelo menos uma operação genuinamente destructive (`delete-entry`). A captura de ecrã confirma isto explicitamente com a etiqueta "DESTRUCTIVE" em ambos os tools write. --- ## 4. As outras 6 integrações Pro de formulários — sem grounding possível Gravity Forms, Ninja Forms, Formidable Forms, MetForm, SureForms e Forminator estão todas listadas em `docs/08-INTEGRACOES-TERCEIROS.md` §7 como classes referenciadas só por `class_exists()` no registrar, com ficheiro fisicamente ausente da árvore Free. Confirmação adicional própria desta tarefa: **nenhum dos dois sites verificados** para este documento (`care.descomplicar.pt` e `ecommerce-demo.descomplicar.pt`) tem qualquer um destes 6 plugins instalado — a listagem completa de `wp plugin list` de ambos os sites (secção 6) não contém `gravityforms`, `ninja-forms`, `formidable`, `metform`, `sureforms`, nem `forminator`/`forminator-pro`. Sem código a ler em lado nenhum, este documento **não propõe** tabelas de ability para estas seis — inventar schemas sem fundamento contradiz o princípio desta série. Ficam confirmadas apenas como ausentes (mesma tabela de gating do doc08 §7, não reproduzida aqui). --- ## 5. Blueprint para réplica ### 5.1 Copiar quase 1:1 - **A classe base `EMCP_Tools_Form_Integration` inteira** (doc08 §3) — contrato `id()/label()/is_active()/operations()`, dispatcher, gate de confirmação genérica, `can_read()=edit_posts`/`can_write()=manage_options`, e a anotação `destructive=true` por omissão no tool write. Nada disto precisa de ser redesenhado; WPForms e Fluent Forms encaixam nele exactamente como CF7 já encaixa. - **O padrão dry-run + `confirm_token` vinculado a fingerprint de estado da Fluent Forms nativa** (secção 2.3) — é uma evolução directa, e comprovadamente já em produção num plugin real, do mecanismo `confirm:true` simples da base EMCP. Vale a pena propor subir este padrão mais rico para a base `EMCP_Tools_Form_Integration` em geral (aplicável também a `woo-orders-write`/etc., doc11 §9), não só a esta integração — a única razão para não o copiar tal-qual é que aumenta a complexidade de qualquer chamador MCP (duas chamadas por escrita em vez de uma). - **Escrever sempre por identificador estável e nunca confiar no valor de retorno de `update()`** — a mesma lição já documentada para ACF (doc08 §1.3) aplica-se ao `WPForms_Form_Handler::update()`: substitui `post_content` inteiro, por isso uma implementação EMCP tem de ler-modificar-escrever com cuidado para não perder campos/notificações não tocados pela operação pedida. - **A marcação `[[UNTRUSTED_USER_INPUT]]` da Fluent Forms nativa** (`MCPHelper::untrusted()`) — um mecanismo directo e barato contra prompt injection via texto submetido por visitantes anónimos, aplicável tal-qual a `get-entry`/`list-entries` de ambas as integrações (WPForms teria o mesmo problema assim que ligado a WPForms Pro real). ### 5.2 Decisão específica para Fluent Forms: detectar e delegar em vez de reimplementar Dado que a versão 6.2.x já embarca `FluentForm\App\Modules\MCP\AbilitiesRegistrar` com abilities reais e testadas (secção 2.3), uma implementação real de `EMCP_Tools_FluentForms_Integration` devia começar por: ```php if ( class_exists( '\FluentForm\App\Modules\MCP\AbilitiesRegistrar' ) ) { // Delegar: reaproveitar getDefinitions() como a fonte de operations(), // sem reimplementar list-forms/get-form/list-submissions/get-submission/ // update-submission-status/delete-submission a partir do zero. } ``` Isto é uma recomendação **exclusiva** desta integração dentro de toda a série de documentos — nenhuma outra (ACF, Meta Box, WooCommerce, Widget Builder, CF7) tem um equivalente nativo já registado via `wp_register_ability()` no próprio plugin de terceiros. O risco a mitigar: a Fluent Forms nativa fica desligada por omissão (`PermissionGate::isEnabled()===false`) e exige um plugin adaptador (`fluent-toolkit`/`mcp-adapter`) — uma implementação EMCP que delegasse teria de activar programaticamente essa option, ou simplesmente reimplementar chamando os mesmos métodos estáticos `FormTools::listForms()`/`SubmissionTools::listSubmissions()` directamente (sem depender do registo MCP nativo estar ligado), o que é possível porque são métodos estáticos públicos. ### 5.3 Decisão específica para WPForms: gate explícita de "entries requer Pro" Dado que `wpforms()->is_pro()` é uma verificação real e barata (`src/WPForms.php`, `apply_filters('wpforms_allow_pro_version', $this->pro)`), as operações `list-entries`/`get-entry`/ `update-entry-status`/`delete-entry` deviam falhar explicitamente com um erro estruturado (`wpforms_pro_required`, análogo ao `plugin_inactive` HTTP 409 já usado pela base para `is_active()===false`, doc08 §3.2) em vez de devolver `[]`/`404` genéricos — a distinção entre "este formulário não existe" e "esta instalação não suporta entradas de todo" é informação que o agente precisa para não repetir a chamada. ### 5.4 Prioridade de construção 1. **`wpforms-read` (list-forms/get-form/list-notifications/get-settings) + `fluentforms-read` (list-forms/get-form)** — zero risco, cobre o caso mais comum ("que formulários existem, o que perguntam, para onde notificam"). Para Fluent Forms, literalmente delegar em `FormTools` (secção 5.2) reduz isto a um wrapper fino. 2. **`fluentforms-read` (list-entries/get-entry)** — Fluent Forms free já guarda submissões nativamente, sem gate Pro-do-plugin-alvo; alto valor imediato. 3. **`wpforms-write` (update-notification)** — única operação de escrita de WPForms com grounding completo nesta instalação; sem depender de WPForms Pro. 4. **`fluentforms-write` (update-entry-status/delete-entry)** — reaproveitar o par dry-run+token da Fluent Forms nativa (secção 5.1) em vez do `confirm:true` simples da base, dado que o código de referência real já demonstra porque vale a pena (fingerprint de estado, replay seguro). 5. **`wpforms-read`/`wpforms-write` (as 4 operações de entries)** — só implementável e testável contra uma instalação WPForms **Pro** real; até lá, ficam atrás da gate `wpforms_pro_required` descrita em 5.3, nunca simuladas. ### 5.5 O que NÃO replicar sem decisão explícita - **Reimplementar do zero o que a Fluent Forms MCP nativa já faz bem** (dry-run/confirm_token, fencing de untrusted input, scope de formulário por utilizador via `FormAccess`) — duplicar essa lógica dentro do EMCP Tools é trabalho redundante e uma segunda superfície para ficar dessincronizada da primeira à medida que a Fluent Forms evolui a sua própria API MCP. - **Simular `list-entries`/`get-entry` de WPForms contra uma instalação Lite** com dados vazios ou de exemplo — o próprio WPForms Lite já mostra dados de amostra na sua UI (secção 1.4) para incentivar upgrade; uma ability MCP que fizesse o mesmo silenciosamente enganaria um agente a pensar que está a ler dados reais. --- ## 6. Fonte **Confirmação de instalação e estado activo** (19-08-2026), via `wp plugin list --format=json`: ``` ssh server "wp plugin list --path=/home/ealmeida/ecommerce-demo.descomplicar.pt --allow-root --format=json" → fluentform active 6.2.12 → wpforms-lite active 2.0.0.5 ``` Listagem prévia de `wp-content/plugins/` de `care.descomplicar.pt` (sem WPForms nem Fluent Forms — descartado como site-alvo) e de `ecommerce-demo.descomplicar.pt` (ambos presentes, `fluentform/` + `wpforms-lite/`) — usada também para confirmar a ausência de Gravity Forms/Ninja Forms/Formidable/ MetForm/SureForms/Forminator em qualquer um dos dois sites (secção 4). **WPForms Lite 2.0.0.5** (`wp-content/plugins/wpforms-lite/`): - `includes/class-form.php` (`WPForms_Form_Handler`, integral) - `src/WPForms.php` (integral — bootstrap, `objects()`, `obj()`, `register()`) - `src/Access/Capabilities.php` (integral) - `src/API.php` (integral — confirma que NÃO é um endpoint REST/MCP) - `includes/functions/access.php` (integral — `wpforms_current_user_can()`, etc.) - `lite/wpforms-lite.php` (integral — notificações/confirmações reais, painel Entries com dados de amostra) - Listagem de directório (sem leitura de conteúdo, confirmação de inventário): `src/` (17 subdirectórios + 4 ficheiros de topo — confirma ausência de qualquer pasta MCP), `includes/functions/` (14 ficheiros), `includes/admin/builder/panels/` (7 ficheiros) **Fluent Forms (free) 6.2.12** (`wp-content/plugins/fluentform/`): - `app/Models/Form.php`, `app/Models/Submission.php` (integrais) - `app/Api/Form.php`, `app/Api/Entry.php` (integrais) - `app/Http/Policies/FormPolicy.php`, `app/Http/Policies/SubmissionPolicy.php` (integrais) - `app/Http/Controllers/McpSettingsController.php` (integral) - `app/Modules/MCP/AbilitiesRegistrar.php` (integral) - `app/Modules/MCP/Tools/FormTools.php`, `app/Modules/MCP/Tools/SubmissionTools.php` (integrais) - `app/Modules/MCP/Support/PermissionGate.php`, `app/Modules/MCP/Support/WriteGuard.php` (integrais) - `app/Modules/Acl/Acl.php` (integral) - Listagem de directório (inventário): `app/` (9 subdirectórios), `app/Models/` (14 ficheiros), `app/Api/` (4 ficheiros), `app/Http/Controllers/` (17 ficheiros), `app/Http/Policies/` (7 ficheiros), `app/Modules/MCP/` (2 ficheiros + 2 subdirectórios), `app/Modules/MCP/Tools/` (8 ficheiros) **Cruzado com**: `docs/00-ARQUITECTURA.md` (padrão `emcp_tools_register_ability()`, `normalize_result`), `docs/08-INTEGRACOES-TERCEIROS.md` §3 (`EMCP_Tools_Form_Integration`, base abstracta real, citada extensivamente), §4 (`EMCP_Tools_CF7_Integration`, única integração concreta real), §7 (tabela de gating das 18 integrações Pro-only, incl. WPForms e Fluent Forms), `docs/10-MODULOS-E-INVENTARIO-PRO.md` linhas 470-476 (confirmação adicional do mesmo gate).