Skip to content

feat(meta-ads): Instant Form lifecycle — status update + duplicate#156

Merged
hyoshi merged 1 commit into
mainfrom
feat/meta-instant-form-lifecycle
May 28, 2026
Merged

feat(meta-ads): Instant Form lifecycle — status update + duplicate#156
hyoshi merged 1 commit into
mainfrom
feat/meta-instant-form-lifecycle

Conversation

@hyoshi
Copy link
Copy Markdown
Collaborator

@hyoshi hyoshi commented May 28, 2026

Summary

Closes #153 (part 2 of 3 of umbrella #151, Meta Instant Form full coverage). Stacked on top of #155 (PR 1) — base branch is feat/meta-instant-form-creative, not main. GitHub will retarget to main automatically when PR 1 merges.

Adds two lifecycle helpers to LeadsMixin: update_lead_form (status only — Meta's post-creation mutability surface has drifted between versions, so we stay conservative) and duplicate_lead_form (no native Meta endpoint; the helper fetches the source and recreates).

Bumps version 0.9.14 → 0.9.15.

What changes

  • mureo/meta_ads/_leads.py — two new mixin methods:

    • update_lead_form(form_id, *, status): POST /{form_id} with {"status": status}. Validates status ∈ {ACTIVE, ARCHIVED} at the helper layer (DRAFT / DELETED / DELETION_PENDING are read-only terminal/initial states and are rejected with ValueError before any API call). Helper docstring explicitly notes that mutating other fields is out of scope — Meta's API surface for post-creation form mutation has shifted between versions (follow_up_action_url in particular), so the conservative choice is "status only".
    • duplicate_lead_form(form_id, *, page_id, new_name): get_lead_form → recreate. Tolerates both the modern privacy_policy: {url, link_text?} dict shape and the legacy privacy_policy_url flat string. Empty url and {link_text: ...} (no url) both surface as ValueError so the operator gets one clear message instead of a Meta 400 later.
    • _LEAD_FORM_FIELDS extended to include privacy_policy (needed by duplicate).
    • Lossy duplication called out in the docstring + the MCP tool description: only questions / privacy_policy / follow_up_action_url / locale are copied. Advanced fields (legal_content_id, gdpr_required / custom_disclaimer, question_page_custom_headline, intro / thank-you screens, conditional question branches) are NOT copied; re-create on the duplicate manually. PR 3 (Meta Instant Form PR 3 — Advanced: CSV export + conditional + multi-step #154) will widen the copied surface.
  • mureo/mcp/_handlers_meta_ads.pyhandle_lead_forms_update + handle_lead_forms_duplicate. Thin pass-through to the helpers.

  • mureo/mcp/_tools_meta_ads_leads.py — two new Tool definitions. Both include the "lossy" / "conservative-mutation" caveats so an LLM caller cannot mistake them for full-CRUD operations.

  • mureo/mcp/tools_meta_ads.py — imports + dispatch table.

  • tests/test_meta_ads_leads.py — 11 new tests:

    • update_lead_form: ARCHIVED happy path, ACTIVE happy path, invalid-status rejection (no API call), _VALID_FORM_STATUSES constant pinned.
    • duplicate_lead_form: full round-trip with new-shape privacy_policy, optional-field skip, legacy flat privacy_policy_url, new_name overrides source name, missing privacy_policy → ValueError, empty url → ValueError, link_text-only → ValueError.
  • tests/test_mcp_tools_meta_ads.pymeta_ads tool count 78 → 80; required_fields parametrize gains entries for lead_forms_update and lead_forms_duplicate; test_all_lead_tools_exist updated 5→7.

  • tests/test_mcp_server.py — total tool count 180 → 182.

  • mureo/_data/skills/_mureo-meta-ads/SKILL.md + skills/_mureo-meta-ads/SKILL.md (synced byte-for-byte) — tool table rows 79/80 + lead_forms reference section gains update and duplicate sub-actions.

  • docs/mcp-server.md — tool table rows.

  • CHANGELOG.md — 0.9.15 entry.

  • pyproject.toml, mureo/__init__.py, .claude-plugin/plugin.json — version bump.

Test plan

  • 11 new tests pass; existing leads tests untouched
  • tests/test_mcp_tools_meta_ads.py and tests/test_mcp_server.py count assertions updated
  • tests/test_plugin_manifests.py::test_packaged_skills_match_canonical_byte_for_byte passes
  • black --check mureo/ clean (211 files unchanged)
  • ruff check clean on all new/modified mureo/ files (the 3 unfixed F841/SIM117 hits in tests/test_meta_ads_leads.py are pre-existing in unrelated handler tests, not introduced by this PR)
  • Code review APPROVED (code-reviewer agent). HIGH (softened immutability overclaim in docstring + tool description) and 3 MEDIUM (lossy duplication doc, tool desc privacy_policy shape, empty-url / link_text-only edge tests) all reflected.
  • CI green

Backward compatibility

Purely additive. New helpers + new MCP tools + extended _LEAD_FORM_FIELDS. No signature changes to existing surface; no storage shape change; no removal. The privacy_policy addition to _LEAD_FORM_FIELDS is a wire-format expansion only — existing callers reading privacy_policy_url keep getting it because Meta returns both when requested.

Out of scope (handled in PR 3 / #154)

  • CSV export helper
  • Advanced form features (conditional questions, multi-step layout, intro / thank-you screens, higher-intent review step)
  • Forwarding legal_content_id / gdpr_required etc. through duplicate

Base automatically changed from feat/meta-instant-form-creative to main May 28, 2026 11:34
Closes #153 (part 2 of 3 of umbrella #151, Meta Instant Form full
coverage). Stacked on top of PR 1 (#155) which lands 0.9.14.

Adds two helpers on LeadsMixin:

- update_lead_form(form_id, *, status) — Meta's lead form API
  surface for post-creation mutation has shifted between versions,
  so the helper conservatively exposes only ``status``
  (ACTIVE / ARCHIVED). Other values are rejected at the helper
  layer with ValueError rather than after a Meta 400 round-trip.
- duplicate_lead_form(form_id, *, page_id, new_name) — Meta has
  no native copy endpoint, so the helper fetches the source form
  and re-creates it. Tolerates both ``privacy_policy: {url}`` and
  the legacy flat ``privacy_policy_url`` string. Lossy by design:
  advanced fields (legal_content_id, gdpr_required,
  question_page_custom_headline, intro/thank-you, conditional
  branches) are NOT copied; doc strings and tool descriptions
  call this out explicitly. PR 3 widens the copied surface.

Two new MCP tools (meta_ads_lead_forms_update,
meta_ads_lead_forms_duplicate) expose the helpers; the
_mureo-meta-ads skill documents both in the tool table and the
lead_forms reference section.

Bumps version 0.9.14 to 0.9.15.

Code-review APPROVED. HIGH softening of immutability claim and
three MEDIUM follow-ups (lossy duplication doc, tool desc
privacy_policy shape, empty-url + link_text-only edge tests)
all reflected in this commit.
@hyoshi hyoshi force-pushed the feat/meta-instant-form-lifecycle branch from b00871b to 476666f Compare May 28, 2026 11:59
hyoshi added a commit that referenced this pull request May 28, 2026
Closes #154 (part 3 of 3 of umbrella #151, Meta Instant Form full
coverage). Stacked on PR 2 (#156) which is stacked on PR 1 (#155).

Two additions on LeadsMixin:

- export_leads_to_csv(form_id, output_path, *, limit=1000,
  field_order=None) -> int — pulls every lead for a form (paginating
  through Meta's cursors automatically) and writes them to a local
  CSV file. Header is [id, created_time, *question_keys]. Standard
  questions without an explicit key map to Meta's lowercased
  field_data[].name (EMAIL -> email). Multi-value answers join with
  " | " so values containing commas stay unambiguous. Defends
  against CSV injection — leading = + - @ \t \r are prefixed with
  a single quote so spreadsheets do not treat user-supplied text
  as a formula. PII never reaches the log; only the row count.

- create_lead_form gains four optional kwargs (all default to
  no-op, existing callers unaffected):
  - context_card: intro / welcome screen (lifts conversion rate)
  - thank_you_page: custom completion screen with CTA (supersedes
    follow_up_action_url's simple redirect)
  - is_higher_intent: 3-step input -> review -> submit form when
    True (trims junk submissions at the cost of total volume)
  - conditional_questions_choices: branching logic so a follow-up
    question only shows when a prior answer matches

Two new MCP-tool entries: meta_ads_leads_export_csv (new) and an
extended meta_ads_lead_forms_create signature (tool name unchanged).
The _mureo-meta-ads skill documents both with usage guidance for
each advanced flag.

Bumps version 0.9.15 to 0.9.16. Code-review APPROVED across two
rounds. First round flagged 2 HIGH (pagination silent truncation,
CSV injection vector) + 4 MEDIUM; second round caught a regression
where the initial pagination fix passed an absolute URL through
_get (which always prepends BASE_URL). Final form extracts the
after cursor via urllib.parse and re-issues on the relative path.
All HIGH/MEDIUM resolved; LOW cosmetic items deferred.
@hyoshi hyoshi merged commit d2079f3 into main May 28, 2026
9 checks passed
@hyoshi hyoshi deleted the feat/meta-instant-form-lifecycle branch May 28, 2026 12:04
hyoshi added a commit that referenced this pull request May 28, 2026
Closes #154 (part 3 of 3 of umbrella #151, Meta Instant Form full
coverage). Stacked on PR 2 (#156) which is stacked on PR 1 (#155).

Two additions on LeadsMixin:

- export_leads_to_csv(form_id, output_path, *, limit=1000,
  field_order=None) -> int — pulls every lead for a form (paginating
  through Meta's cursors automatically) and writes them to a local
  CSV file. Header is [id, created_time, *question_keys]. Standard
  questions without an explicit key map to Meta's lowercased
  field_data[].name (EMAIL -> email). Multi-value answers join with
  " | " so values containing commas stay unambiguous. Defends
  against CSV injection — leading = + - @ \t \r are prefixed with
  a single quote so spreadsheets do not treat user-supplied text
  as a formula. PII never reaches the log; only the row count.

- create_lead_form gains four optional kwargs (all default to
  no-op, existing callers unaffected):
  - context_card: intro / welcome screen (lifts conversion rate)
  - thank_you_page: custom completion screen with CTA (supersedes
    follow_up_action_url's simple redirect)
  - is_higher_intent: 3-step input -> review -> submit form when
    True (trims junk submissions at the cost of total volume)
  - conditional_questions_choices: branching logic so a follow-up
    question only shows when a prior answer matches

Two new MCP-tool entries: meta_ads_leads_export_csv (new) and an
extended meta_ads_lead_forms_create signature (tool name unchanged).
The _mureo-meta-ads skill documents both with usage guidance for
each advanced flag.

Bumps version 0.9.15 to 0.9.16. Code-review APPROVED across two
rounds. First round flagged 2 HIGH (pagination silent truncation,
CSV injection vector) + 4 MEDIUM; second round caught a regression
where the initial pagination fix passed an absolute URL through
_get (which always prepends BASE_URL). Final form extracts the
after cursor via urllib.parse and re-issues on the relative path.
All HIGH/MEDIUM resolved; LOW cosmetic items deferred.
hyoshi added a commit that referenced this pull request May 28, 2026
Closes #154 (part 3 of 3 of umbrella #151, Meta Instant Form full
coverage). Stacked on PR 2 (#156) which is stacked on PR 1 (#155).

Two additions on LeadsMixin:

- export_leads_to_csv(form_id, output_path, *, limit=1000,
  field_order=None) -> int — pulls every lead for a form (paginating
  through Meta's cursors automatically) and writes them to a local
  CSV file. Header is [id, created_time, *question_keys]. Standard
  questions without an explicit key map to Meta's lowercased
  field_data[].name (EMAIL -> email). Multi-value answers join with
  " | " so values containing commas stay unambiguous. Defends
  against CSV injection — leading = + - @ \t \r are prefixed with
  a single quote so spreadsheets do not treat user-supplied text
  as a formula. PII never reaches the log; only the row count.

- create_lead_form gains four optional kwargs (all default to
  no-op, existing callers unaffected):
  - context_card: intro / welcome screen (lifts conversion rate)
  - thank_you_page: custom completion screen with CTA (supersedes
    follow_up_action_url's simple redirect)
  - is_higher_intent: 3-step input -> review -> submit form when
    True (trims junk submissions at the cost of total volume)
  - conditional_questions_choices: branching logic so a follow-up
    question only shows when a prior answer matches

Two new MCP-tool entries: meta_ads_leads_export_csv (new) and an
extended meta_ads_lead_forms_create signature (tool name unchanged).
The _mureo-meta-ads skill documents both with usage guidance for
each advanced flag.

Bumps version 0.9.15 to 0.9.16. Code-review APPROVED across two
rounds. First round flagged 2 HIGH (pagination silent truncation,
CSV injection vector) + 4 MEDIUM; second round caught a regression
where the initial pagination fix passed an absolute URL through
_get (which always prepends BASE_URL). Final form extracts the
after cursor via urllib.parse and re-issues on the relative path.
All HIGH/MEDIUM resolved; LOW cosmetic items deferred.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meta Instant Form PR 2 — Lifecycle: status update + duplicate

1 participant