feat(meta-ads): Instant Form lifecycle — status update + duplicate#156
Merged
Conversation
8 tasks
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.
b00871b to
476666f
Compare
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.
8 tasks
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.
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, notmain. GitHub will retarget tomainautomatically 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) andduplicate_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}. Validatesstatus ∈ {ACTIVE, ARCHIVED}at the helper layer (DRAFT / DELETED / DELETION_PENDING are read-only terminal/initial states and are rejected withValueErrorbefore 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_urlin particular), so the conservative choice is "status only".duplicate_lead_form(form_id, *, page_id, new_name):get_lead_form→ recreate. Tolerates both the modernprivacy_policy: {url, link_text?}dict shape and the legacyprivacy_policy_urlflat string. Emptyurland{link_text: ...}(no url) both surface asValueErrorso the operator gets one clear message instead of a Meta 400 later._LEAD_FORM_FIELDSextended to includeprivacy_policy(needed by duplicate).questions/privacy_policy/follow_up_action_url/localeare 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.py—handle_lead_forms_update+handle_lead_forms_duplicate. Thin pass-through to the helpers.mureo/mcp/_tools_meta_ads_leads.py— two newTooldefinitions. 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_STATUSESconstant pinned.duplicate_lead_form: full round-trip with new-shape privacy_policy, optional-field skip, legacy flatprivacy_policy_url, new_name overrides source name, missing privacy_policy → ValueError, empty url → ValueError, link_text-only → ValueError.tests/test_mcp_tools_meta_ads.py—meta_adstool count 78 → 80;required_fieldsparametrize gains entries forlead_forms_updateandlead_forms_duplicate;test_all_lead_tools_existupdated 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 gainsupdateandduplicatesub-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
tests/test_mcp_tools_meta_ads.pyandtests/test_mcp_server.pycount assertions updatedtests/test_plugin_manifests.py::test_packaged_skills_match_canonical_byte_for_bytepassesblack --check mureo/clean (211 files unchanged)ruff checkclean on all new/modifiedmureo/files (the 3 unfixed F841/SIM117 hits intests/test_meta_ads_leads.pyare pre-existing in unrelated handler tests, not introduced by this PR)code-revieweragent). 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.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. Theprivacy_policyaddition to_LEAD_FORM_FIELDSis a wire-format expansion only — existing callers readingprivacy_policy_urlkeep getting it because Meta returns both when requested.Out of scope (handled in PR 3 / #154)
legal_content_id/gdpr_requiredetc. through duplicate