--- name: mcp-wpforms description: MCP dedicado multi-site (node stdio, ligação `wpforms` em ~/.omp/agent/mcp.json) para gerir o WPForms + WPForms Form Abandonment em descomplicar.pt e emanuelalmeida.pt via WP-CLI/SQL/SSH — formulários, entries (listar/ler/contar/marcar lida/estrela/spam), entries abandonadas com contexto capturado, settings gerais (captcha, licença), estado Akismet/spam. Site é um parâmetro em cada tool. Única documentação de mapeamento de dados WPForms — não existe skill de conhecimento separada. Usar quando "wpforms mcp", "entries wpforms", "formulário abandonado", "form abandonment", "spam wpforms", "akismet wpforms", "schema formulário wpforms", "wpforms_entries", "marcar entry como lida", "settings wpforms". layer: wiki --- # /mcp-wpforms — MCP dedicado multi-site ao WPForms + Form Abandonment Projecto em `/media/ealmeida/Dados/Dev/mcp-wpforms/` (TypeScript, SDK MCP oficial, stdio). Sem código PHP novo no WordPress: cada tool executa `wp db query` (SELECTs `JSON_OBJECT`/`JSON_ARRAYAGG` e `UPDATE`s de 1 coluna), `wp option get` ou `wp post get` via `ssh server` sobre o WP-CLI já instalado. `site` é um parâmetro em cada tool, não uma ligação fixa — como `mcp-wpfc`/`mcp-wpmeteor`. Não existe skill de conhecimento "wpforms" separada (só o EMCP Tools referencia o ícone do addon Form Abandonment, nada mais). **Esta skill é a única documentação do mapeamento de dados WPForms** neste repositório — inclui aqui o schema das tabelas e das options que noutros MCPs (ex. `wp-fastest-cache` ↔ `mcp-wpfc`) viveria numa skill "irmã". ## Âmbito Formulários (leitura do schema), entries (listar/ler/contar, marcar lida/estrela/spam), entries abandonadas do addon Form Abandonment, settings gerais (captcha, comportamento) e licença (mascarada), estado Akismet. **Não cobre**: criação/edição do schema de campos de um formulário (risco de corromper o formulário — fora de âmbito por desenho), reenvio de notificações de email, tabelas de analytics/payments/tasks_meta/file_restrictions/protected_files do WPForms (não usadas por nenhuma tool), o plugin `styler-for-wpforms` (inactivo em emanuelalmeida.pt). ## Sites conhecidos Chamar `wpforms_list_sites` para a lista actual. Ao contrário de `mcp-wpfc`, **não** aceita um path arbitrário — as queries SQL directas precisam do prefixo de tabela real do site (`table_prefix`), que varia por instalação e não pode ser adivinhado com segurança. Só sites explicitamente verificados são aceites. Confirmado ao vivo 19-08-2026 (`wp plugin list`, `wp config get table_prefix`): | Alias | Domínio | Path | `table_prefix` | WPForms | Form Abandonment | |---|---|---|---|---|---| | `descomplicar` | descomplicar.pt (produção) | `/home/ealmeida/public_html` | `wpah_` | 2.0.0.4, activo, licença Elite | 1.16.0, activo | | `emanuelalmeida` | emanuelalmeida.pt | `/home/ealmeida/emanuelalmeida.pt` | `wpne_` | 2.0.0.4, activo | não instalado | Akismet não está instalado em nenhum dos dois sites (`wpforms_get_spam_status` devolve `akismet_plugin_installed: false`). ## Mapeamento de dados (confirmado por leitura directa do código-fonte e da BD) ### Formulários — `wp_posts` (post_type = `wpforms`) O schema de cada formulário (campos + settings) vive em `post_content` como **texto JSON puro** (não serializado PHP) — `{"fields": {...}, "settings": {...}, "id", "field_id", "search_terms", "providers", "meta"}`. `fields` é um objecto indexado por field-id (não um array), cada entrada com pelo menos `id`, `type`, `label`, `required`. As settings interessantes: `form_abandonment` (`"1"` = activo), `form_abandonment_fields` (`""` = só guarda se tiver email/telefone, `"all"` = guarda sempre), `form_abandonment_duplicates` (`"1"` = evita duplicados na mesma hora), `store_spam_entries` (`"0"`/`"1"`), `akismet` (só existe se o plugin Akismet estiver instalado — nenhum dos 2 sites tem). `wpforms_get_form_schema` extrai `fields` para uma lista simplificada (id/type/label/required/ description) e devolve `settings` completo tal-e-qual. `wpforms_list_forms` usa `JSON_EXTRACT`/`JSON_UNQUOTE` directamente sobre `post_content` (sem descodificar o JSON completo) para expor `form_abandonment_enabled`/`store_spam_entries` de forma barata em massa — truque reutilizável para qualquer outra flag de settings sem custo de ler o schema completo por formulário. ### Entries — tabelas custom `{prefix}wpforms_entries` / `entry_fields` / `entry_meta` / `logs` `wpforms_entries`: `entry_id`, `form_id`, `post_id`, `user_id`, `status`, `type`, `viewed` (tinyint), `starred` (tinyint), `fields` (longtext — JSON `{field_id: {name, value, id, type}}`, os dados submetidos tal como o WPForms os mostra no admin), `meta` (longtext, **não usado** em produção — sempre vazio, não confundir com a tabela `entry_meta`), `date`, `date_modified`, `ip_address`, `user_agent`, `user_uuid`. **`status` — valores confirmados por leitura do código-fonte** (não documentados centralmente pelo próprio plugin, espalhados por `src/Pro/AntiSpam/SpamEntry.php` e pelo addon Form Abandonment): - `''` (string vazia) — entry normal. - `'abandoned'` — capturada pelo addon Form Abandonment (`WPFormsFormAbandonment\Plugin::process_entries`). - `'spam'` — marcada como spam (`SpamEntry::ENTRY_STATUS`). Desmarcar spam **repõe sempre `''`**, mesmo que a entry estivesse antes `abandoned`/`trash` — é o comportamento nativo de `SpamEntry::set_as_not_spam`, replicado tal-e-qual em `wpforms_set_entry_spam`. - `'trash'` — enviada para o lixo. `entry_fields`: uma linha por (entry, field) para pesquisa/filtragem indexada — não usada por nenhuma tool deste MCP (a coluna `fields` da própria entry já tem os mesmos dados, mais legível). `entry_meta`: linhas `{entry_id, form_id, user_id, status, type, data, date}`. `type` observados: `log` (histórico tipo "Entry read."), e — só em entries `abandoned` — `page_url`, `page_title`, `page_id`, `url_referer`, `user_id` (o contexto capturado pelo Form Abandonment no momento do abandono). `wpforms_list_abandoned_entries` faz o pivot destes 4 tipos para campos directos na resposta. `logs` (`{prefix}wpforms_logs`): log administrativo do plugin (título/mensagem/tipo), não exposto por nenhuma tool — fora do âmbito pedido. ### Settings / licença — `wp_options` `wpforms_settings` — array PHP nativo (`a:20:{...}`), **seguro usar `--format=json`** em leitura (confirmado por `wp option get wpforms_settings --format=json`, sem dupla codificação). Contém `captcha-provider` (`recaptcha`/`hcaptcha`/`turnstile`/vazio), chaves reCAPTCHA/hCaptcha/Turnstile, `modern-markup`, etc. `wpforms_license` — também array nativo: `key`, `type` (`elite` em descomplicar.pt), `is_expired`. `wpforms_get_settings` mascara sempre a chave de licença (só os últimos 4 caracteres visíveis), nunca a devolve em claro. ## Tools (12) | Tool | Read-only | Uso | |---|---|---| | `wpforms_list_sites` | sim | Aliases conhecidos, path, `table_prefix`, se tem Form Abandonment | | `wpforms_list_forms` | sim | Todos os formulários (qualquer post_status): título, datas, `form_abandonment_enabled`, `store_spam_entries`, `entry_count` | | `wpforms_get_form_schema` | sim | Schema completo de 1 formulário: campos simplificados + `settings` completo do `post_content` | | `wpforms_list_entries` | sim | Entries com filtros `form_id`/`status` (normal/abandoned/spam/trash)/`date_from`/`date_to`, paginado — resumo sem `fields`/`meta` | | `wpforms_get_entry` | sim | 1 entry completa: dados + `fields` submetidos + toda a `entry_meta` (log, contexto de abandono, razão de spam) | | `wpforms_count_entries` | sim | Contagem por estado (`total`/`normal`/`abandoned`/`spam`/`trash`), agrupada por formulário | | `wpforms_list_abandoned_entries` | sim | Entries `abandoned` com `fields` parciais + `page_url`/`page_title`/`url_referer`/`captured_user_id` (pivot de `entry_meta`) | | `wpforms_get_settings` | sim | `wpforms_settings` (captcha, geral) + resumo de licença (chave sempre mascarada) | | `wpforms_get_spam_status` | sim | Akismet instalado/activo, contagem global de spam, `store_spam_entries` + contagem de spam por formulário | | `wpforms_set_entry_viewed` | não | `entries.viewed` — mirror da acção "Read"/"Unread" | | `wpforms_set_entry_starred` | não | `entries.starred` — mirror da acção "Star"/"Unstar" | | `wpforms_set_entry_spam` | não | `entries.status` — mirror exacto de `SpamEntry::set_as_spam`/`set_as_not_spam` (ver nota sobre repor `''`) | ## Segurança Escritas restritas a `UPDATE` de 1 coluna (`viewed`, `starred`, `status`) sobre `entries`, sempre precedidas de leitura para confirmar que a entry existe (`requireEntry`, lança erro claro se não). Nunca escreve schema de formulário nem `post_content`. Identificadores de tabela/coluna validados por `assertSafeIdentifier` (allowlist `[a-z0-9_]+`); filtros de texto livre (datas) escapados por `sqlString`; texto SQL viaja sempre pelo stdin do processo SSH, nunca interpolado na linha de comando remota. Site restrito à allowlist de `sites.ts` (sem path arbitrário) precisamente para impedir uma query SQL correr contra um prefixo de tabela errado. ## Verificação Construído e testado ponta-a-ponta contra produção 19-08-2026, via chamadas reais ao protocolo MCP (stdio) e confirmação cruzada por SQL directo fora do MCP: - `wpforms_list_forms` em `descomplicar` (1 formulário, "Pedido de Orçamento", 26 entries) e `emanuelalmeida` (6 formulários, ex. "Formulário de contacto" com 17 entries) — confirmados por `SELECT COUNT(*)`/`wp post list` directos antes de qualquer chamada ao MCP. - `wpforms_count_entries` em `descomplicar`: `total=26, normal=11, abandoned=13, spam=0, trash=2` — igual ao `GROUP BY status` feito directamente na BD. - `wpforms_get_entry` (entry 37) e `wpforms_list_abandoned_entries` — campos submetidos (email, telefone, empresa) e contexto de abandono (`page_url`, `page_title`) conferem com as colunas `fields`/`entry_meta` lidas directamente. - `wpforms_get_form_schema` (formulário 16446) — 157 campos extraídos correctamente do `post_content`, incluindo `layout`/`pagebreak`. - `wpforms_get_settings` — `captcha-provider: "recaptcha"` e licença `type: "elite"` com chave mascarada, confirmados contra `wp option get wpforms_settings/wpforms_license --format=json`. - `wpforms_get_spam_status` em ambos os sites — Akismet correctamente reportado como não instalado, `store_spam_entries` extraído por `JSON_EXTRACT` a bater com o `post_content` bruto. - Escrita testada e confirmada por SQL directo antes/depois: `wpforms_set_entry_viewed(descomplicar, entry_id=37, viewed=true)` sobre uma entry já `viewed=1` — round-trip idempotente, valor inalterado em produção, sem qualquer efeito colateral. - Erros testados: formulário/entry inexistente devolvem mensagem clara (`isError: true`) em vez de rebentar ou devolver dados parciais. ## Skills relacionadas Nenhuma skill de conhecimento "wpforms" existe neste repositório — ver secção "Mapeamento de dados" acima para o schema completo. `mcp-wpfc`/`mcp-wpmeteor` — mesmo padrão de MCP dedicado multi-site sobre WP-CLI/SSH, para outros plugins do bundle.