Files
emcp-tools-mapping/docs/13-FORMS-PRO-BLUEPRINT.md
T
Claude Code 2812cd79ba docs: doc13 Forms Pro (WPForms+Fluent Forms) + doc14 Project Memory blueprints
Fundamentados em código real: WPForms Lite 2.0.0.5 + Fluent Forms 6.2.12
(activos em ecommerce-demo.descomplicar.pt), e CARL+Mem0/Hindsight (sistemas
de memória/governança já em produção neste ecossistema). Achado central doc13:
Fluent Forms já embarca servidor MCP nativo próprio (AbilitiesRegistrar, 9
abilities, dry-run+confirm_token) - recomenda delegar em vez de reimplementar.
INDEX.md actualizado com as 2 novas entradas e totais.
2026-08-19 07:58:31 +01:00

41 KiB

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):

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):
    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:
    $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<string,array> — mapa nome => { mode, run, perm, desc, confirm? } — e a base fornece register() (regista os dois tools <id>-read/<id>-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:

// 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:

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).