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.
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:
- A classe base + CF7, código real Free já lido (
docs/08-INTEGRACOES-TERCEIROS.md§3-§4) — o contratoid()/label()/is_active()/operations(), o dispatcherdispatch(), 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 quedestructive=trueé o default do toolwritedesta base específica (diferente de ACF/Meta Box/SEO) — citado tal-qual do doc08. - 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Ã]. - 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:
- O registry nunca regista
'entry'.src/WPForms.php::objects()(chamado emplugins_loaded):Apenaspublic function objects(): void { $this->registry['form'] = new WPForms_Form_Handler(); $this->registry['process'] = new WPForms_Process(); do_action( 'wpforms_loaded' ); }formeprocess.obj( 'entry' )/obj( 'entry_meta' )/obj( 'entry_fields' )devolvem semprenull(return $this->registry[$name] ?? null;) — o próprioWPForms_Form_Handler::delete()já lida com isto defensivamente:Em Lite este$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 */ }ifé sempre falso — não há nada para apagar porque não há classe registada. - A página admin "Entries" mostra dados FALSOS, codificados no plugin.
lite/wpforms-lite.php, métodoget_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_loadedaction +fluentform/mcp_ability_namesfilter."
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_Integrationinteira (doc08 §3) — contratoid()/label()/is_active()/operations(), dispatcher, gate de confirmação genérica,can_read()=edit_posts/can_write()=manage_options, e a anotaçãodestructive=truepor 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_tokenvinculado 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 mecanismoconfirm:truesimples da base EMCP. Vale a pena propor subir este padrão mais rico para a baseEMCP_Tools_Form_Integrationem geral (aplicável também awoo-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 aoWPForms_Form_Handler::update(): substituipost_contentinteiro, 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 aget-entry/list-entriesde 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
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 emFormTools(secção 5.2) reduz isto a um wrapper fino.fluentforms-read(list-entries/get-entry) — Fluent Forms free já guarda submissões nativamente, sem gate Pro-do-plugin-alvo; alto valor imediato.wpforms-write(update-notification) — única operação de escrita de WPForms com grounding completo nesta instalação; sem depender de WPForms Pro.fluentforms-write(update-entry-status/delete-entry) — reaproveitar o par dry-run+token da Fluent Forms nativa (secção 5.1) em vez doconfirm:truesimples da base, dado que o código de referência real já demonstra porque vale a pena (fingerprint de estado, replay seguro).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 gatewpforms_pro_requireddescrita 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-entryde 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).