Documenta o MCP dedicado mcp-fluentform (descomplicar.pt + starter.descomplicar.pt): 9 tools sobre a camada de serviço oficial do plugin (FormService/ SubmissionService/GlobalSettingsService), mapeamento de tabelas custom fluentform_*, gotchas confirmados ao vivo (serialização de datas Carbon-like, race condition em escritas paralelas dependentes) e evidência de verificação ponta-a-ponta contra produção/staging.
168 lines
13 KiB
Markdown
168 lines
13 KiB
Markdown
---
|
|
name: mcp-fluentform
|
|
description: MCP dedicado multi-site (node stdio, ligação `fluentform` em ~/.omp/agent/mcp.json) para o FluentForms 6.2.12 (WPManageNinja) em descomplicar.pt (produção) e starter.descomplicar.pt (staging) — formulários, submissions (entries), contagens por estado e settings gerais, via WP-CLI/SSH sobre a camada de serviço oficial do plugin (FormService/SubmissionService/GlobalSettingsService). Escritas limitadas a estado da entry (unread/read/spam/trashed) e favorita/star — nunca schema de formulário nem integrações de terceiros. Usar quando "fluentforms", "formulários wordpress", "entries fluentform", "submissions fluentform", "marcar entry como lida", "favoritar entry", "spam fluentform", "trash entry fluentform", "contagem de submissions", "settings fluentforms", ou qualquer execução (não estratégia) sobre o FluentForms.
|
|
layer: wiki
|
|
---
|
|
|
|
# /mcp-fluentform — MCP dedicado FluentForms (multi-site)
|
|
|
|
Projecto em `/media/ealmeida/Dados/Dev/mcp-fluentform/` (TypeScript, SDK MCP oficial, stdio). Não
|
|
existe skill de conhecimento separada sobre o FluentForms — esta skill é a ÚNICA documentação,
|
|
incluindo o mapeamento de dados (tabelas/serviços) que noutros casos vive numa skill "irmã" (ver
|
|
`fluent-crm`↔`mcp-fluent-crm` para esse padrão).
|
|
|
|
**Achado relevante desta sessão:** o FluentForms 6.2.12 já traz o seu próprio módulo MCP interno
|
|
(`app/Modules/MCP/`, abilities `fluentform/list-submissions`, `fluentform/update-submission-status`,
|
|
etc., via WP Abilities API), mas só fica acessível externamente se o site tiver o plugin
|
|
`mcp-adapter` ou `fluent-toolkit` activo e o toggle "MCP" ligado nas definições do FluentForms —
|
|
nenhuma das duas condições está preenchida hoje nos dois sites. Este MCP **não depende disso**:
|
|
chama directamente a camada de serviço PHP subjacente (`FormService`/`SubmissionService`/
|
|
`GlobalSettingsService`), a mesma que o módulo MCP nativo do plugin também usa por baixo.
|
|
|
|
## Sites conhecidos
|
|
|
|
Chamar `fluentform_list_sites` para a lista actual. Confirmados ao vivo 19-08-2026, FluentForms
|
|
6.2.12 active nos dois — são os únicos dois sites do bundle com o plugin activo (`wp plugin list`):
|
|
|
|
| Alias | Domínio | Prefixo de tabela | Formulários | Submissions |
|
|
|---|---|---|---|---|
|
|
| `descomplicar` | descomplicar.pt (produção) | `wpah_` | 3 (`Contact Form Demo` unpublished, `Subscription Form` unpublished, `Blank Form (#3)` published) | 1 (form #3, status `read`) |
|
|
| `starter` | starter.descomplicar.pt (staging) | `wpbk_` | 2 (`Contact Form Demo`, `Subscription Form`, ambos published) | 0 |
|
|
|
|
Também aceita um path absoluto directamente (`/home/ealmeida/<site>`) para sites fora desta lista,
|
|
sem exigir rebuild do MCP — mas confirmar primeiro que o FluentForms está activo lá
|
|
(`wp plugin list --allow-root --path=<path>`).
|
|
|
|
## Mapeamento de dados
|
|
|
|
FluentForms não expõe WP-CLI CRUD (`wp fluentform` só tem `activate_license`/`license_status`/
|
|
`stats`). Toda a leitura/escrita deste MCP passa pela camada de serviço PHP oficial do plugin, via
|
|
`wp eval` — nunca SQL directo, nunca `wp option update`.
|
|
|
|
**Tabelas custom** (prefixo de tabela varia por site, ver acima; resolvido internamente pelo ORM
|
|
do plugin, nunca hardcoded em TypeScript):
|
|
|
|
| Tabela (sem prefixo) | Papel |
|
|
|---|---|
|
|
| `fluentform_forms` | formulários — `title`, `status` (`published`/`unpublished`), `type`, `form_fields` (JSON do schema), `has_payment` |
|
|
| `fluentform_submissions` | entries — `form_id`, `response` (JSON das respostas), `status` (`unread`/`read`/`spam`/`trashed`), `is_favourite`, `serial_number`, `user_id`, `payment_*` |
|
|
| `fluentform_submission_meta` | meta por submission (ex. `_entry_uid_hash` para link público) |
|
|
| `fluentform_entry_details` | valores desnormalizados por campo (usado por relatórios/exportação) |
|
|
| `fluentform_form_meta` | meta por formulário (settings, notificações, integrações) |
|
|
| `fluentform_form_analytics` | contadores de vistas/conversão por formulário |
|
|
| `fluentform_logs` | log de eventos por submission/formulário |
|
|
|
|
**Classes de serviço oficiais usadas por este MCP** (`FluentForm\App\Services\*` +
|
|
`FluentForm\App\Models\*`, todas instanciáveis directamente com `new`, sem container DI):
|
|
|
|
- `Form::query()` — listagem/filtro de formulários (`orderBy`, `where('title','LIKE',...)`, `where('status',...)`, `->paginate($perPage,['*'],'page',$page)`).
|
|
- `FormService::getInputsAndLabels($formId)` — schema de campos (`inputs`/`labels`), usado por `fluentform_get_form` e para rotular os valores em `fluentform_get_submission`.
|
|
- `Submission::customQuery($attrs)` — o MESMO método que o wp-admin usa para a listagem de entries; aceita `form_id`, `entry_type` (nome real do parâmetro de estado — não `status`; `'favorites'` é tratado à parte via `is_favourite=1`), `search`, `date_range: [from,to]`, `sort_by`.
|
|
- `SubmissionService::find($id)` — detalhe completo de 1 entry. **EFEITO SECUNDÁRIO REAL DO PLUGIN:** marca automaticamente `unread`→`read` (mesmo comportamento de abrir a entry no wp-admin) — documentado na descrição da tool, não é bug deste MCP.
|
|
- `SubmissionService::updateStatus(['entry_id'=>,'status'=>])` — `unread`/`read`/`spam`/`trashed`; dispara `do_action('fluentform/after_submission_status_update', ...)`.
|
|
- `SubmissionService::toggleIsFavorite($id)` — toggle puro, devolve `[mensagem, novo_estado]`.
|
|
- `SubmissionService::resources(['form_id'=>,'counts'=>true])` → `Submission::countByGroup($formId)` — contagens `unread`/`read`/`spam`/`trashed`/`all` (exclui trashed)/`favorites`.
|
|
- `GlobalSettingsService::get(['key'=>[...]])` — só lê chaves com prefixo `fluentform_`/`_fluentform_`/`fluentform-`/`_fluentform-` (allowlist própria do plugin); usado por `fluentform_get_settings`.
|
|
|
|
## Arquitectura — `wp eval` sobre a camada de serviço, nunca SQL/WP-CLI directo
|
|
|
|
Corpo PHP fixo (nunca construído a partir de texto do chamador) + dados de entrada (JSON) viajam
|
|
em base64 dentro de uma variável de ambiente remota, nunca interpolados como texto dinâmico no
|
|
comando SSH — mesmo padrão de `mcp-fluent-crm/src/wp-bridge.ts` (`runPhp()`), adaptado a
|
|
multi-site: o path WordPress é um parâmetro por chamada (`src/sites.ts`), não uma config fixa.
|
|
|
|
**Gotcha de datas — não replicar sem testar primeiro:** `$model->created_at`/`updated_at` não
|
|
devolvem uma string — devolvem um objecto `FluentForm\Framework\Support\DateTime` (Carbon-like).
|
|
`json_encode()` directo sobre esse objecto produz `{"date":"...","timezone_type":3,"timezone":"..."}`
|
|
em vez de uma string, porque o `serializeDate()` custom do `Model.php` do plugin só é aplicado em
|
|
`toArray()`/`toJson()`, nunca num `json_encode()` PHP nativo isolado. Corrigido em todos os pontos
|
|
deste MCP com `(string) $form->created_at` — o `DateTime` do framework implementa `__toString()`
|
|
devolvendo `Y-m-d H:i:s` correctamente.
|
|
|
|
**Gotcha de race condition — confirmado ao vivo durante a construção:** duas chamadas de escrita
|
|
dependentes (ex. dois `fluentform_toggle_favorite` seguidos, para testar round-trip) NUNCA podem
|
|
correr em paralelo — cada `wp eval` é um processo PHP efémero independente; disparar dois
|
|
concorrentes faz ambos lerem o mesmo estado "antigo" e o resultado final fica indeterminado (visto
|
|
ao vivo: 1º toggle devolveu `is_favourite:true`, mas a leitura SQL imediatamente a seguir mostrou
|
|
`0` porque o 2º toggle, corrido em paralelo, tinha lido o `0` original em vez do `true` já escrito
|
|
pelo 1º). Corrigido correndo sequencialmente; qualquer teste/uso futuro deste MCP com múltiplas
|
|
escritas na mesma entry deve aguardar o resultado de cada chamada antes da seguinte.
|
|
|
|
## Tools (9)
|
|
|
|
| Tool | Read-only | Uso |
|
|
|---|---|---|
|
|
| `fluentform_list_sites` | sim | Aliases conhecidos, path WP, prefixo de tabela |
|
|
| `fluentform_list_forms` | sim | `search`/`status`(published\|unpublished)/`sort_by`/paginação — id, título, estado, tipo, `has_payment`, datas |
|
|
| `fluentform_get_form` | sim | Detalhe de 1 formulário + schema de campos (`key`/`label`/`element`) — necessário para saber as chaves esperadas em `fluentform_get_submission` |
|
|
| `fluentform_list_submissions` | sim | Entries de UM formulário (`form_id` obrigatório) — filtros `status`(unread\|read\|spam\|trashed\|favorites)/`search`/`date_from`+`date_to`(YYYY-MM-DD)/`sort_by`, paginação |
|
|
| `fluentform_get_submission` | **efeito secundário** | Detalhe completo de 1 entry (campos rotulados). Marca `unread`→`read` automaticamente (comportamento real do plugin) |
|
|
| `fluentform_count_submissions` | sim | Contagens por estado de um formulário: `unread`/`read`/`spam`/`trashed`/`all`/`favorites` |
|
|
| `fluentform_update_submission_status` | não | `unread`/`read`/`spam`/`trashed`. `trashed` é soft-delete (reversível chamando de novo com outro estado) |
|
|
| `fluentform_toggle_favorite` | não | Toggle puro — chamar duas vezes seguidas (sequencialmente!) devolve ao estado original |
|
|
| `fluentform_get_settings` | sim | Options com allowlist `fluentform_`/`_fluentform_` — por omissão: `_fluentform_global_form_settings` (layout/misc), `fluentform_global_modules_status` (integrações ligadas/desligadas, nunca credenciais), `_fluentform_installed_version`. Aceita `keys` para ler chaves específicas dentro do mesmo allowlist |
|
|
|
|
Todas as tools exigem `site` como primeiro parâmetro.
|
|
|
|
## Decisão deliberada: sem escrita de schema/integrações
|
|
|
|
Nenhuma tool cria/edita formulários, campos, notificações ou integrações de terceiros
|
|
(Mailchimp/Slack/etc). Razão (do enunciado desta tarefa): risco alto, fora do âmbito de um MCP de
|
|
gestão de entries — mudar `form_fields`/integrações parte a experiência de submissão pública do
|
|
site sem forma fácil de detectar isso a partir de fora. Gerir formulários/integrações via
|
|
wp-admin (**FluentForms → Forms/Settings**).
|
|
|
|
## Segurança
|
|
|
|
- Nenhuma tool constrói SQL ou nomes de tabela a partir de input do chamador — todas as queries
|
|
passam por métodos do ORM do próprio plugin (`Form::query()`, `Submission::customQuery()`,
|
|
`Model::find()`), nunca `whereRaw`/interpolação directa.
|
|
- `fluentform_get_settings` só lê chaves com o prefixo allowlisted pelo próprio
|
|
`GlobalSettingsService` — não há forma de ler uma option arbitrária fora do namespace
|
|
FluentForms através deste MCP.
|
|
- Corpo PHP + payload sempre em base64 dentro de env var remota — nunca interpolados como texto no
|
|
comando SSH (ver `src/wp-bridge.ts`).
|
|
- `fluentform_get_submission` devolve valores de campos submetidos pelo público (texto não
|
|
confiável) sem qualquer fencing adicional — ao contrário do módulo MCP nativo do plugin (que
|
|
aplica marcadores `[[UNTRUSTED_USER_INPUT]]`), porque este MCP é de uso interno da equipa, não
|
|
exposto a um agente autónomo com acesso à internet pública. Reavaliar se o uso mudar.
|
|
|
|
## Verificação
|
|
|
|
Construído e testado ponta-a-ponta contra produção e staging em 19-08-2026, via protocolo MCP real
|
|
(stdin/stdout, não só chamadas SSH soltas):
|
|
|
|
- `npm run build` limpo (tsc, sem erros).
|
|
- Smoke test stdio: `initialize` + `tools/list` devolveu os 9 tools esperados.
|
|
- Leituras reais confirmadas por SQL directo fora do MCP: `fluentform_list_forms` em
|
|
`descomplicar` (3 formulários) e `starter` (2 formulários, ambos `published`);
|
|
`fluentform_get_form` no formulário #3 (`descomplicar`) devolveu 8 campos incluindo um campo
|
|
FluentBooking (`fcal_booking`); `fluentform_list_submissions` no formulário #3 (`descomplicar`,
|
|
1 entry, `status:read`); `fluentform_count_submissions` no mesmo formulário
|
|
(`{unread:0,read:1,spam:0,trashed:0,all:1,favorites:0}`); `fluentform_get_settings` em
|
|
`descomplicar` devolveu `_fluentform_global_form_settings`/`fluentform_global_modules_status`/
|
|
`_fluentform_installed_version` correctos.
|
|
- `fluentform_get_submission` na entry #1 (`descomplicar`): 7 campos rotulados correctamente
|
|
(nome, email, empresa, cargo, data de reserva FluentBooking, 2 checkboxes), datas devolvidas como
|
|
string `Y-m-d H:i:s` (não o objecto Carbon cru).
|
|
- **Escrita 1 (idempotente):** `fluentform_update_submission_status` na entry #1 com
|
|
`status:"read"` (valor já-corrente) — devolveu `{entry_id:1,status:"read"}`, confirmado por SQL
|
|
directo que `status`/`is_favourite` ficaram inalterados (só `updated_at` mudou, efeito normal de
|
|
qualquer update).
|
|
- **Escrita 2 (round-trip):** `fluentform_toggle_favorite` na entry #1 chamado sequencialmente duas
|
|
vezes — 1ª chamada devolveu `is_favourite:true` (confirmado `is_favourite=1` por SQL directo),
|
|
2ª chamada devolveu `is_favourite:false` (confirmado `is_favourite=0` por SQL directo, estado
|
|
final idêntico ao inicial).
|
|
- Tratamento de erro confirmado: `fluentform_get_form` com `form_id:9999` devolveu
|
|
`isError:true` com "Formulário #9999 não encontrado."; `fluentform_list_forms` com
|
|
`site:"unknown-site"` devolveu `isError:true` listando os aliases conhecidos.
|
|
|
|
## Skills relacionadas
|
|
|
|
- `mcp-fluent-crm` — mesmo mecanismo `wp eval` sobre camada de serviço oficial, para contactos e
|
|
campanhas em descomplicar.pt; FluentCRM tem automações que disparam a partir de submissões
|
|
FluentForm (`FluentFormSubmissionTrigger`).
|
|
- `mcp-fluent-smtp` — transporte de email usado pelas notificações de submissão do FluentForms.
|
|
- `mcp-wpfc` / `mcp-wpmeteor` — outros MCPs multi-site do mesmo bundle, padrão `site` como
|
|
parâmetro em cada tool.
|