Repository navigation
docs(security): document GHSA-2v2f-mvfg-ph56 in SECURITY.md - #139
Merged
Merged
Conversation
Add the X-Pipeboard-Token auth-bypass advisory entry (fixed in 1.0.115) alongside the existing GHSA-45gf and GHSA-9gw6 entries. Documentation only; the fix already shipped.
PM-Labs
pushed a commit
to pmlabs-org/mcp-meta-ads
that referenced
this pull request
Jun 11, 2026
…d-co#139) Add the X-Pipeboard-Token auth-bypass advisory entry (fixed in 1.0.115) alongside the existing GHSA-45gf and GHSA-9gw6 entries. Documentation only; the fix already shipped.
Herriaan
added a commit
to Herriaan/meta-ads-mcp
that referenced
this pull request
Jun 17, 2026
* feat: add attribution_spec support to create_adset and update_adset (pipeboard-co#60) * Bump version to 1.0.71 * docs: warn that attribution_spec is immutable after ad set creation * Bump version to 1.0.72 * Bump version to 1.0.73 (fix server.json version for MCP Registry) * feat: return regional_regulation fields from get_adset_details and get_adsets (pipeboard-co#61) Add regional_regulated_categories and regional_regulation_identities to the fields requested by get_adset_details and get_adsets, so users can read verified identity IDs from existing ad sets when creating new ones in regulated markets (Singapore, Taiwan, Australia, India, Thailand). Also bump version to 1.0.74. * fix: support description field for video creatives via asset_feed_spec * fix: support description field for video creatives via asset_feed_spec Meta API v24 rejects description inside video_data directly. Previously, passing description with a video creative silently ignored the field. Now, when video_id + description are both provided, the code routes through asset_feed_spec (which does support descriptions) instead of the simple video_data path. Image creatives are unchanged: description still goes into link_data.description as before. Added tests: - test_video_creative_with_description: verifies asset_feed_spec path - test_video_creative_description_only: verifies minimal video+description case - Added assertions for headline and description in simple image creative test Also fixed pre-existing test failures in test_duplication*.py where PIPEBOARD_API_BASE_URL from the dev environment was overriding the production URL expected by URL-assertion tests. * Bump version to 1.0.74 * Bump version to 1.0.75 * fix: expand optimization_goal docs for OUTCOME_ENGAGEMENT and fix flaky URL tests (pipeboard-co#64) * Bump version to 1.0.76 (pipeboard-co#65) * fix: image_crops valid keys + new compute_image_crops tool (pipeboard-co#67) * fix image_crops docstring: list valid keys, remove invalid 191x100 example, add 1080x1080 coordinates The previous docstring showed 191x100 as a valid example crop key. Meta API only accepts 6 crop keys: 100x100, 100x72, 400x500, 400x150, 600x360, 90x160. Passing 191x100 causes all creatives to fail with generic Invalid parameter. Updated the image_crops parameter docstring to: - List all 6 valid keys with their use cases - Explicitly warn that 191x100 is NOT valid and to use 600x360 for horizontal placements - Provide pre-computed original-crop coordinates for a 1080x1080 source image - Clarify coordinate format (pixel-based, top-left and bottom-right corners) * add compute_image_crops tool: compute correct crop coordinates for any image dimensions Adds a new compute_image_crops MCP tool that returns ready-to-use image_crops coordinates for create_ad_creative and bulk_create_ad_creatives. The tool is pure computation (no API call, no auth required). For each of the 6 valid Meta API crop keys (100x100, 100x72, 400x500, 400x150, 600x360, 90x160), it computes the largest centered crop box that fits within the source image while matching the key ratio. This is equivalent to the Ads Manager "Original" crop behavior. - Replaces the hardcoded 1080x1080 table in the docstring with a pointer to this tool - Warns and skips invalid keys (e.g. 191x100) with guidance to use 600x360 instead - No auth required (pure math) - 15 new tests covering geometry correctness, edge cases, and the MCP tool interface * fix: remove unnecessary 191x100 warnings and no-auth note from crop docs The 191x100 warnings are no longer needed since the docs now list only valid crop keys. The "no authentication" note is redundant. * Bump version to 1.0.77 * Fix/bid strategy adset budget (pipeboard-co#70) * fix: skip bid_strategy when using adset-level budgets Prevents Meta API error 1885737 by only sending bid_strategy when the campaign owns its budget. bid_strategy defaults to LOWEST_COST_WITHOUT_CAP so the condition was always true, even for adset-level budget campaigns. * fix: auto-downgrade DOF when asset_customization_rules is provided Meta silently ignores asset_customization_rules for DEGREES_OF_FREEDOM creatives. Now optimization_type is automatically removed so placement rules take effect under regular dynamic creative mode, and a warning field is returned in the response. Also includes asset_customization_rules in the use_asset_feed decision so rules aren't ignored when only a singular image_hash is provided. * feat: add object_story_id support to create_ad_creative (pipeboard-co#68) * feat: add object_story_id and disable_all_enhancements to create_ad_creative * fix: use individual enhancement keys for disable_all_enhancements (standard_enhancements deprecated) * Bump version to 1.0.78 * create_adset: detect CBO budget conflict before hitting Meta API (pipeboard-co#71) When daily_budget or lifetime_budget is provided to create_adset and the parent campaign already has a budget (Campaign Budget Optimization / CBO mode), Meta rejects the request with a cryptic Invalid parameter error (subcode 1885621). This change adds a pre-flight check that fetches the campaign budget fields alongside the existing bid_strategy check (one combined API call). If the campaign already has a budget, a clear error is returned before the create call is made, including the campaign name, conflict explanation, and fix instructions. Also improves the daily_budget and lifetime_budget docstrings with a CBO note so LLMs are less likely to include budget fields in CBO adset requests. Also fixes two pre-existing duplication test failures caused by PIPEBOARD_API_BASE_URL being set to localhost in the dev environment. 14 new unit tests added in tests/test_cbo_budget_conflict.py. * Bump version to 1.0.79 * feat: add event_id parameter to create_ad_creative for EVENT_RESPONSES campaigns (pipeboard-co#72) * feat: add event_id parameter to create_ad_creative for EVENT_RESPONSES campaigns Adds event_id to link_data so event RSVP/ticket ads render the event card correctly. Also included in call_to_action.value when call_to_action_type is EVENT_RSVP or BUY_TICKETS, per Meta's documented payload for ticket ads. Applied to both the DOF anchor link_data and the traditional simple-image link_data paths. * test: add E2E test for event_id parameter in create_ad_creative Verifies event_id is persisted in both link_data.event_id and call_to_action.value.event_id on read-back from the real Meta API. Uses sys.path insertion to ensure the local package is loaded instead of any site-packages install. * Bump version to 1.0.80 * add reminder_data support to create_ad_creative for Instagram Reminder Ads (pipeboard-co#73) Adds reminder_data parameter to create_ad_creative, placing inline event data (event_name, start_time, end_time) inside object_story_spec.link_data. This enables the REMINDERS_SET optimization goal without requiring an existing ig_upcoming_event_id (upcoming_events path). Changes: - New reminder_data parameter (dict or JSON string, coerced automatically) - Applied to simple image path and DOF/DEGREES_OF_FREEDOM path - link_url validation now also accepts reminder_data as a valid anchor - 5 new tests covering all reminder_data scenarios - Fix pre-existing test failures in test_duplication and test_duplication_regression caused by PIPEBOARD_API_BASE_URL env var not being pinned in those tests * Bump version to 1.0.81 (pipeboard-co#74) * Add account_id param to search_interests, get_interest_suggestions, search_behaviors (pipeboard-co#75) Account-restricted Meta API tokens require account_id to be passed to the /search endpoint. Without it, Meta returns: Please specify the account_id parameter. This API token is restricted to a specific ad account. - search_interests: add optional account_id param, include in API call when set - get_interest_suggestions: same fix - search_behaviors: same fix - tests: add account_id test cases for all three functions * Revert account_id param from targeting search tools (pipeboard-co#76) The bug was caused by Pipeboard own ACL gate, not the Meta API. Adding these tools to CROSS_ACCOUNT_TOOLS in route.ts is the correct fix. * Bump version to 1.0.82 (pipeboard-co#77) * feat: support placement asset customization (videos[], images[], optimization_type=PLACEMENT) (pipeboard-co#79) - Remove validation that blocked optimization_type values other than DEGREES_OF_FREEDOM. PLACEMENT is a valid Meta API value needed for per-placement asset customization. - Add videos[] and images[] parameters to create_ad_creative. Each entry supports a label field that is converted to adlabels for use with asset_customization_rules video_label/image_label references. - Fix DOF vs PLACEMENT routing: PLACEMENT creatives use non-DOF pattern (link_urls and ad_formats in asset_feed_spec, bare object_story_spec) while DOF keeps its existing link_data anchor pattern. - Fix DOF auto-downgrade to only apply when optimization_type is DEGREES_OF_FREEDOM, not for PLACEMENT or other values. * docs+fix: warn when DOF mode is used with multiple image_hashes (pipeboard-co#78) * docs+fix: warn when DOF mode is used with multiple image_hashes Meta silently serves only ONE image at delivery time when optimization_type=DEGREES_OF_FREEDOM is combined with multiple image_hashes, despite accepting the spec without error. - Add warning to image_hashes and optimization_type docstrings clarifying the DOF single-image limitation - Add runtime check: return a structured warning response when DOF + len(image_hashes) > 1 is detected, blocking the call before a wasted API request - Update tests: test_dof_with_multiple_image_hashes_returns_warning asserts the new warning response; other DOF tests updated to use single image_hash (the valid DOF usage pattern) - Fix pre-existing test failures in test_duplication.py and test_duplication_regression.py caused by PIPEBOARD_API_BASE_URL env var leaking into tests that expected the production default URL * docs: warn about DOF + multiple image_hashes silently serving one image Meta silently serves only ONE image at delivery time when optimization_type=DEGREES_OF_FREEDOM is combined with multiple image_hashes, despite accepting the spec without error. Updated docstrings on image_hashes and optimization_type params to document this limitation. No runtime guard (pass-through to Meta API). Pre-existing test fixes: test_duplication and test_duplication_regression failures caused by PIPEBOARD_API_BASE_URL env var leaking from dev env. * docs: add warning field to create_ad_creative response for DOF + multiple image_hashes Call proceeds normally — warning is included in the success response so the LLM knows Meta will only serve one image at delivery time. * Bump version to 1.0.83 (pipeboard-co#80) * Add pre-commit hook to block email addresses in staged changes (pipeboard-co#81) Public repo protection: scans diffs for email patterns and blocks commits unless the address is in .email-allowlist. * fix create_ad_creative: route video+instagram_actor_id through asset_feed_spec to avoid error 1443048 (pipeboard-co#82) Meta returns error 1443048 (object_story_spec ill formed) when instagram_user_id is set in object_story_spec for a video creative without ad_formats=["SINGLE_VIDEO"] present in asset_feed_spec. Either field alone is insufficient - both must be present together. Fix: extend use_asset_feed condition to include (video_id and instagram_actor_id). This ensures video creatives with an Instagram actor ID always route through asset_feed_spec, which automatically includes ad_formats=["SINGLE_VIDEO"] for video creatives. Also updated docstrings for video_id and instagram_actor_id to document this requirement and the auto-handling behavior. Tests: updated existing instagram_actor_id test to verify asset_feed_spec is present with ad_formats=["SINGLE_VIDEO"]; added two new tests for explicit ad_formats and regression coverage for the plain video path (no instagram_actor_id stays on simple path). * Bump version to 1.0.84 (pipeboard-co#83) * feat: add branded content params to create_ad_creative (pipeboard-co#84) Add facebook_branded_content and instagram_branded_content as optional dict parameters. Passed through as top-level fields on the creative payload to Meta adcreatives endpoint. Includes JSON string coercion for MCP transports that deliver dicts as strings. facebook_branded_content: {sponsor_page_id: <page_id>} for Facebook partnership ads instagram_branded_content: {sponsor_id: <ig_user_id>} for Instagram partnership ads * fix(ads): guard video thumbnail auto-fetch on video_id, not is_video (pipeboard-co#86) When create_ad_creative is called with videos=[...] (plural) but no singular video_id, the auto-fetch block was reached because is_video was true; it then called make_api_request(None, ...), which Meta rejects with a generic error and the warning logs Could not auto-fetch thumbnail for video None. Tighten the guard to require video_id to be truthy. Per-video thumbnail fetching for the videos[] loop is intentionally out of scope here and will be addressed in a follow-up PR. Add a regression test asserting that the plural videos[] form does not trigger the singular auto-fetch. Split out from pipeboard-co#85. * fix(ads): translate placement_groups rules for videos[] path (pipeboard-co#87) When callers use videos=[] with asset_customization_rules in placement_groups format, translate the rules to Meta API shape (customization_spec with publisher_platforms + facebook_positions/instagram_positions, video_label at rule level) and add adlabels to videos[] entries. Previously the rules were shipped to Meta untranslated, causing error 1487390 (generic Adcreative Create Failed) because Meta does not understand placement_groups or video_ids inside customization_spec. Parallels the existing _translate_asset_customization_rules for images[]. Also coerces customization_spec.video_label: str to {name: str} at the rule level, and preserves any adlabels the caller already set via videos[].label. No API version change needed — verified end to end on v24 (default). Creative created successfully with optimization_type PLACEMENT and correct per-video placement targeting. * fix(ads): auto-fetch thumbnails for videos[] entries in create_ad_creative (pipeboard-co#88) Meta API v24 requires a thumbnail (image_hash or image_url) for each entry in asset_feed_spec.videos[]. Without one, creates fail with error 1443226 (Please specify one of image_hash or image_url in the video_data field of object_story_spec). Until now, only the singular video_id path auto-fetched a thumbnail; callers using the plural videos=[...] form had to provide thumbnail_url for every entry. This change extracts the existing fetch into a small _fetch_video_thumbnail helper and applies it to every videos[] entry that lacks thumbnail_url. Fetches run in parallel via asyncio.gather to avoid N sequential round trips for N videos. The helper prefers thumbnails.data[0].uri over picture (the pre-generated URI is more reliable; picture can be a small placeholder), and returns None on failure so the caller can decide whether to proceed. * Bump version to 1.0.85 (pipeboard-co#90) * Bump version to 1.0.85 * Bump version to 1.0.85 (remaining files) * fix(ads): drop video_data from object_story_spec when asset_feed_spec.videos[] is used (Meta 1443048) (pipeboard-co#89) * fix(ads): drop video_data from object_story_spec when asset_feed_spec.videos[] is used (Meta 1443048) When create_ad_creative builds an asset_feed_spec.videos[] payload (the use_asset_feed branch with video_id), the previous code emitted a dual shape: the video lived both in asset_feed_spec.videos[] and in an object_story_spec.video_data anchor. Meta API v24 rejects this dual shape with error 1443048 ("object_story_spec ill formed"). Per Meta documentation, the canonical shape for asset_feed_spec.videos[] is bare page_id (plus instagram_user_id when set) in object_story_spec. The video, thumbnail, link URL, and CTA all live in asset_feed_spec. Ref: https://developers.facebook.com/docs/marketing-api/dynamic-creative/dynamic-creative-optimization This collapses the video_id and non-DOF image branches into one bare-page_id block. The DOF image branch (which still requires a link_data anchor) is unchanged. The simple-video path (use_asset_feed=False) is also unchanged: it still uses video_data inside object_story_spec. Tests updated explicitly (per reviewer concerns from a prior PR): - 4 existing tests on the asset_feed_spec path had video_data assertions that are no longer valid; assertions are rewritten with PR-C comments naming the change and referencing fbtraces. - 3 new tests pin the new contract: video_id + plurals, video_id + singular description (the case where the code routes through asset_feed_spec), and video_id + instagram_actor_id all assert object_story_spec keys are exactly {page_id} or {page_id, instagram_user_id}. E2E verified against Meta v24 in our test account using bare page_id shape: ISSUE 1 (singular video + plural messages) returned creative 937436332460350 successfully; ISSUE 4 (singular video + singular description) returned creative 1272282771659661 successfully. Posting the prior dual shape directly to Meta v24 reproduced 1443048 (fbtrace AC3CmeSWCCiBf8hbELlhdOI). * test: minimize PR-C test diff - relocate CTA/link assertions instead of deleting - Trim verbose comments in 4 modified tests to a single PR-C reference each - Use oss == {expected_dict} instead of separate set(oss.keys()) checks - Drop 3 new tests (bare_page_id_*, singular_description_uses_bare_page_id) whose contracts are already pinned by the modified existing tests - Relocate link_url and (where applicable) CTA assertions to asset_feed_spec * Bump version to 1.0.86 * feat(adsets): expose bid_adjustments + bump to 1.0.87 (pipeboard-co#91) * feat(adsets): expose bid_adjustments on get/create/update Add bid_adjustments as a pass-through parameter on create_adset and update_adset, and include it in the fields= query on get_adsets and get_adset_details. Shape: {user_groups: {<dim>: {<value>: <float>, default: <float>}}}. Writing requires Meta app capability allowlisting. * Bump version to 1.0.87 * docs: remove RELEASE.md (pipeboard-co#92) The release process references private infrastructure and lives in internal documentation. External contributors: refer to the GitHub Releases page and PyPI for available versions. * fix(ads): detect silent single-image collapse in create_ad_creative (pipeboard-co#93) When create_ad_creative posts multi-image asset_feed_spec with asset_customization_rules, Meta can accept the POST and return a creative id without error, yet persist only a single-image object_story_spec. The handler now compares the readback against the posted payload and surfaces a warning when the multi-image feed was not preserved. Also revises the DOF downgrade warning to avoid claiming rules always take effect, and adds pytest cases for the new detection path. Bumps version to 1.0.88. * fix(ads): reuse existing video adlabel in placement_groups rules (pipeboard-co#94) When the caller provides videos with explicit labels (videos[].label) AND asset_customization_rules that reference those videos via video_ids, the translator now reuses the explicit adlabel as the rule video_label instead of minting a fresh PBOARD_VID_N. Meta rejected the prior mismatch shape with error_subcode 2446173 (Target rule label does not refer to any of the asset labels) and sometimes with the generic 1487390 (Adcreative Create Failed). Happy path unchanged: when no explicit adlabels are set on the videos, PBOARD_VID_N minting still runs and Meta accepts the creative. Regression covered by two new tests in test_video_creatives.py. * docs(ads): rewrite DOF-downgrade warning to name PAC instead of is_dynamic_creative (pipeboard-co#95) The previous wording told callers that placement rules would 'depend on the target ad set is_dynamic_creative capability'. That conflates two modes: is_dynamic_creative governs regular Dynamic Creative, while an asset_customization_rules-bearing creative with optimization_type dropped ends up in Placement Asset Customization, which routes by placement without needing is_dynamic_creative on the ad set. The new warning says so explicitly and points callers who actually wanted Advantage+ auto-optimization at removing asset_customization_rules instead. Bumps version to 1.0.90. * docs(ads): clarify link_url is required when asset_customization_rules is set (pipeboard-co#97) Meta enforces link_urls at create_ad time (error 1885800) whenever a creative uses asset_customization_rules, including Lead ads. Docstring previously claimed link_url was optional for Lead ads, which is only true when rules are absent. * docs(ads): expand call_to_action_type enum list in create_ad_creative (pipeboard-co#99) Free-form CTA values (e.g. MAKE_RESERVATION, BOOK_TABLE) are rejected by Meta with code 100. The previous docstring listed only four examples, which left the LLM guessing at the right enum value for restaurants and similar verticals. Expands the docstring with the documented set of common Meta CTA values and explicitly notes that BOOK_NOW is the correct choice for restaurant reservations / appointments. Doc-only change. No behavior change. * fix(ads): emit call_to_actions plural with lead_gen_form_id on asset_feed_spec path (pipeboard-co#100) When call_to_action_type and lead_gen_form_id (or phone_number) are both set on the asset_feed_spec path (videos[]/images[] + asset_customization_rules, PLACEMENT optimization), emit Metas call_to_actions plural object array carrying value.lead_gen_form_id instead of the string-only call_to_action_types array. The string-only carrier silently drops the form id at creation time, leaving the creative without a lead form. Downstream create_ad then fails with error_subcode 3390001 (Missing Lead Form). The plural object carrier preserves the form id and is what Metas Ads Manager UI emits for the same shape. Same fix applied to the object_story_id (existing-post) branch when rules are present. Bumps version to 1.0.91. Tests: 28/28 in tests/test_video_creatives.py; full creative suite 108/108. * fix(ads): accept {text,adlabels} headlines/descriptions/messages and drop client-side length guards (pipeboard-co#101) Two related fixes for create_ad_creative + update_ad_creative when used with asset_customization_rules: 1. headlines / descriptions / messages now accept either plain strings (existing behavior) OR objects {text, adlabels:[{name}]}, mirroring what videos[] and images[] already support. The dict form is required when rules reference title_label / body_label / description_label — without per-entry adlabels, Meta rejects with subcode 1885878 (Multiple titles assets cannot be applied to rule) or 2446173 (Target rule label doesnt refer to any of the asset labels). Added a _normalize_text_variants helper that handles both shapes. Threading wired through both the create_ad_creative asset_feed_spec branch and the update_ad_creative asset_feed_spec path. 2. Removed the four client-side guards that rejected >5 headlines/descriptions and 40-char headlines / 125-char descriptions. Per the no-pre-flight- validation rule, never strip or filter user-provided params before sending to Meta. The Meta UI accepts strings the guard rejected (verified live 2026-04-30 against act_1276764704512927: 41-char headline returned 200 OK and was stored verbatim). Bumps version to 1.0.92. Tests: 111 in the full creative suite. Existing length/count validation tests were inverted to assert the new pass-through behavior; 4 new positive tests cover the dict-form titles, descriptions, bodies, and the str/dict mixed case. * feat(duplicate_creative): add new_creative_features_spec override param (pipeboard-co#102) Lets callers pass individual feature opt-ins/opt-outs on the duplicate (e.g. image_touchups, image_uncrop, text_optimizations, video_auto_crop with {enroll_status: OPT_IN | OPT_OUT}). Forwarded through to the Next.js duplication service which merges the override on top of the source creatives surviving creative_features_spec entries. Pairs with the Next.js side which now (a) surfaces a structured warning when the source creative had standard_enhancements (Meta deprecated this key for POST with subcode 3858504, re-verified 2026-04-30), naming what got stripped and what was preserved, and (b) accepts new_creative_features_spec to let the caller restore equivalent semantics via individual feature keys. Bumps version to 1.0.93. Tests: 440 passed across the full suite (excluding external API). Live e2e verified against act_1276764704512927: source 26624513227234313 + override produced duplicate 962587122816028 with the expected merged dof spec on readback (image_touchups OPT_OUT overrode source OPT_IN, image_uncrop OPT_IN added, text_optimizations OPT_IN preserved). * fix(insights,accounts): platform_position breakdown + fields passthrough on get_account_info (pipeboard-co#103) get_insights with breakdown=platform_position used to fail with Meta (pipeboard-co#100) error: platform_position must be paired with publisher_platform and is incompatible with the default action_breakdowns=[action_type]. Auto-prepend publisher_platform when platform_position is requested alone, and drop the action-typed fields (actions, action_values, conversions, cost_per_action_type) so the request succeeds with placement-level metrics. get_account_info now accepts an optional fields parameter so callers can fetch funding_source_details, spend_cap, is_prepay_account, and other Graph fields needed to reconcile pre-paid balances. Default field set unchanged when fields is omitted. Bump version to 1.0.94. * docs(insights): remove unsupported media_* breakdowns from the supported list (pipeboard-co#105) * fix(insights): drop action-typed fields for media_* breakdowns get_insights with breakdown=media_asset_url, media_creator, media_destination_url, media_format, media_origin_url, media_text_content, or media_type was failing with Meta error pipeboard-co#100 (Current combination of data breakdown columns (action_type, X) is invalid). Adding these to _BREAKDOWNS_INCOMPATIBLE_WITH_ACTION_TYPE so the request drops actions, action_values, cost_per_action_type, and conversions when one of them is passed. * feat(insights): surface dropped fields and reason as a note in the response Follow-up to pipeboard-co#104. When breakdown is one of the seven media_* dimensions that conflict with action_type, get_insights drops actions, action_values, cost_per_action_type, and conversions from the request. Without surfacing this, callers see asset rows but no signal that action metrics are missing by design (vs. a bug). The response now includes a top-level note (human-readable) and dropped_fields (machine-readable) when the drop applies. * docs(insights): remove unsupported media_* breakdowns from the supported list Probing Meta directly with action_breakdowns=[] (overriding the implicit action_type) reveals the underlying error: 'Tried accessing nonexisting field (media_asset_url)'. The seven media_* names (media_asset_url, media_creator, media_destination_url, media_format, media_origin_url, media_text_content, media_type) are not valid Meta Insights breakdowns. They were advertised in the breakdown docstring but never worked. Removes them from the documented Creative Assets list and notes the asset breakdowns only return data for Dynamic Creative ads. Reverts code changes from the closed PR pipeboard-co#104 that tried to drop action-typed fields for these names — that was a no-op for the user since Meta rejected regardless. * chore(release): bump to 1.0.95 * docs(insights): restore media_type to supported list, bump 1.0.96 (pipeboard-co#106) PR pipeboard-co#105 (v1.0.95) lumped media_type in with the 6 truly-nonexistent media_* names. Direct Meta probe shows media_type is a real Insights breakdown (returns 200 with action_breakdowns=[]; collides with the default action_type, hence the (pipeboard-co#100) action_type collision Davide saw). The other six (media_asset_url, media_creator, media_destination_url, media_format, media_origin_url, media_text_content) are genuinely nonexistent fields. Restores media_type to the Creative Assets line and rewrites the note to cite only the nonexisting-field error. * feat(insights): action_breakdowns override + auto-empty for media_type (pipeboard-co#107) Meta rejects (action_type, media_type) with (pipeboard-co#100) invalid breakdown columns. Auto-set action_breakdowns=[] when breakdown=media_type so the request succeeds (empty rows for non-DCO ads, real rows for DCO ads). Add an action_breakdowns parameter so callers can override the heuristic or pass other Meta-supported values (action_target_id, action_destination, etc.). Bumps version 1.0.96 -> 1.0.97. * fix(creatives): strip deprecated standard_enhancements from GET responses (pipeboard-co#109) Meta still emits standard_enhancements inside creative_features_spec on GET responses but rejects it on POST with error_subcode 3858504. LLM clients frequently copy GET payloads straight into the next mutation, which silently re-introduces the deprecated key and blocks creative writes. Strip the key from get_creative_details and get_ad_creatives at both the top-level creative_features_spec and degrees_of_freedom_spec.creative_features_spec so the model never sees it. Bumps version to 1.0.98. * docs(create_campaign): expand tool description, bump to 1.0.99 (pipeboard-co#110) * feat(ads): return effective_status and issues_info on get_ads and get_ad_details, bump to 1.0.100 (pipeboard-co#111) status reflects only the configured ACTIVE/PAUSED state, so callers cannot detect NOT_DELIVERING, DISAPPROVED, WITH_ISSUES, or PENDING_REVIEW from get_ads. Add effective_status and issues_info to the field list in all three branches of get_ads (account, campaign, adset) and on get_ad_details so delivery state and policy issues come through by default. * feat(campaigns): add adset_budgets to update_campaign for CBO to ABO migration, bump to 1.0.101 (pipeboard-co#112) Meta's documented mechanism for disabling the Advantage campaign budget and assigning per-ad-set budgets is a single POST to /CAMPAIGN_ID with adset_budgets=[{adset_id, daily_budget}, ...]. The previous workaround of sending empty daily_budget/lifetime_budget via use_adset_level_budgets=true returns success but Meta silently ignores the empty values, so the campaign-level budget persists and ad set budget updates keep failing with Budget Conflict. Add adset_budgets parameter, document the migration in the tool description, and clarify in the response note that use_adset_level_budgets is not reliable for the migration. * chore(release): sync server.json version + enforce parity with pyproject.toml (pipeboard-co#113) * chore(release): sync server.json to 1.0.101 and enforce parity with pyproject.toml in CI server.json had drifted to 1.0.93 while pyproject.toml is at 1.0.101. The MCP Registry publish step has been failing with cannot publish duplicate version on every release since (v1.0.99, .100, .101 all rejected) because the registry already has 1.0.93 from a prior publish. Catch server.json up to 1.0.101 and replace the no-op validate-version CI step with a real check that fails the build when pyproject.toml and server.json versions diverge. * fix(ci): drop tomllib (Python 3.11+) for grep so 3.10 job works * Translate deprecated instagram_actor_id to instagram_user_id in create_ad_creative (pipeboard-co#114) * Translate deprecated instagram_actor_id to instagram_user_id in create_ad_creative Meta deprecated instagram_actor_id at POST /act_ID/adcreatives in Jan 2026. Sending the field returns error 100: 'Param instagram_actor_id must be a valid Instagram account id' verbatim, regardless of whether the ID is correct. The replacement is instagram_user_id. The object_story_spec path already migrated it (nested inside the spec), but the object_story_id path had no object_story_spec and was still sending the deprecated field at the top level. This change adds the translation in that path and updates the misleading error enhancement that previously blamed a missing instagram_basic scope. Added three tests in tests/test_object_story_id.py covering the object_story_id + instagram_actor_id, object_story_id + asset_customization_rules + instagram_actor_id, and the updated error enhancement paths. * chore(release): bump to 1.0.102 * fix(create_ad_creative): serialize CALL_NOW phone_number as call_to_action.value.link tel: URI (pipeboard-co#116) Meta Marketing API v24 rejects a literal phone_number key inside call_to_action.value with code 100 ("Invalid keys phone_number were found in param call_to_action[value]"). The supported shape is call_to_action.value.link = "tel:<E.164 number>". This change updates all six CALL_NOW serialization paths in create_ad_creative (existing-post + asset_customization_rules, existing-post simple, DOF/PLACEMENT asset_feed_spec image, simple image link_data, simple video video_data, plus a second asset_feed_spec branch) and refreshes the docstring + regression tests in tests/test_call_now_phone_number.py. * chore(release): bump to 1.0.103 for CALL_NOW tel: URI fix (PR pipeboard-co#116) (pipeboard-co#117) * chore(deps): bump mcp in the pip group across 1 directory (pipeboard-co#121) Bumps the pip group with 1 update in the / directory: [mcp](https://github.com/modelcontextprotocol/python-sdk). Updates `mcp` from 1.12.2 to 1.23.0 - [Release notes](https://github.com/modelcontextprotocol/python-sdk/releases) - [Changelog](https://github.com/modelcontextprotocol/python-sdk/blob/main/RELEASE.md) - [Commits](modelcontextprotocol/python-sdk@v1.12.2...v1.23.0) --- updated-dependencies: - dependency-name: mcp dependency-version: 1.23.0 dependency-type: direct:production dependency-group: pip ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(create_ad_creative): single video + IG actor -> object_story_spec for CTWA (pipeboard-co#119) Route single-video + Instagram-actor creatives through object_story_spec.video_data instead of asset_feed_spec so CTWA campaigns (OUTCOME_SALES/OUTCOME_ENGAGEMENT, destination=WHATSAPP) accept them. Drop link from the WHATSAPP_MESSAGE CTA value (Meta error_subcode 1815630). Normalize optimization_type=REGULAR to no-op. Sanitized test fixtures. Bump to 1.0.104. Full suite: 450 passed. * fix(streamable-http): pin mount to /mcp/ + bump to 1.0.106 (pipeboard-co#122) The mcp SDK defaults streamable_http_path to /mcp (no trailing slash). The upstream proxy at internal nginx :9009 forwards Next.js requests to /mcp/ verbatim, which the SDK responds to with HTTP 307 to /mcp. That redirect dance is the prime suspect for the 1.0.104 production transport regression (PagerDuty incident 129); reverted to 1.0.103 on the same day, hotfixed as 1.0.105 without the mcp 1.23.0 bump. This release retries the mcp 1.23.0 bump with the redirect removed: explicitly setting streamable_http_path=/mcp/ makes the with-slash URL canonical, so prod traffic hits a direct 200 instead of a 307. Pairs with --host 127.0.0.1 in the systemd unit (separate repo) for deterministic IPv4 binding. * fix(transport): disable DNS rebinding protection + bump to 1.0.107 (pipeboard-co#123) The 1.0.106 canary on mcp2 returned HTTP 421 Invalid Host header for all proxied traffic. mcp 1.23.0 auto-enables transport_security with a port-wildcard allowlist (127.0.0.1:*, localhost:*, [::1]:*). The internal nginx upstream uses proxy_set_header Host with the bare host variable, which strips the port — so the forwarded Host (127.0.0.1) does not match the allowlist patterns and every request is rejected. The protection is irrelevant for a loopback-only service that external clients cannot reach. Disable it explicitly via transport_security.enable_dns_rebinding_protection=False so the allowlist constraint stops blocking proxied traffic. Direct curl on mcp2 confirms: with the disable applied locally, Host: 127.0.0.1 (no port) returns 200; without it, 421. * feat(transport): drop streamable_http_path override + bump to 1.0.108 (pipeboard-co#124) The override pinned the mount to /mcp/ to match the (incorrect) assumption that prod forwarded /mcp/. Verified via internal-nginx upstream access log that prod META_MCP_BACKEND_URLS is /mcp (no trailing slash), so the SDK default streamable_http_path = /mcp matches prod and the override is unnecessary. Removing it avoids the 307 redirect under prod traffic that detached the undici ArrayBuffer on the Next.js proxy retry path (1.0.106 regression). Keeps transport_security.enable_dns_rebinding_protection = False (required when internal nginx strips the port from the Host header, so the SDKs loopback allowlist 127.0.0.1:* never matches). Keeps mcp[cli]==1.23.0 pin. * fix(security): reject unauthenticated HTTP requests + redact tokens in error URLs (GHSA-9gw6-46qc-99vr) (pipeboard-co#125) Two issues affected operators running --transport streamable-http with a META_ACCESS_TOKEN env var on a network-reachable port: - AuthInjectionMiddleware forwarded requests with no auth headers, so any caller could invoke MCP tools using the operator env-var token. - make_api_request() serialized request URLs verbatim into error payloads; Meta sends access_token as a query param, so a Graph 4xx echoed the long-lived operator credential back to the caller. Fixes: 1. Middleware now returns 401 + WWW-Authenticate: Bearer when neither Authorization Bearer nor X-PIPEBOARD-API-TOKEN header is present. 2. _redact_url() strips access_token and appsecret_proof from the url and request_url fields of error responses. Adds SECURITY.md with reporting policy and the advisory entry, including operator action items (upgrade + rotate any token that may have been exposed via earlier error responses). Bumps to 1.0.109. * docs(create_ad_creative): promote Placement Asset Customization to a named mode (pipeboard-co#126) The PAC path (optimization_type=PLACEMENT + labelled videos[]/images[] + asset_customization_rules) was documented only inside individual parameter docs. Add it as a first-class mode in the top-of-docstring summary so callers can discover the dual-aspect, non-Dynamic-Creative shape without reading every parameter description. Notes the label-matching requirement and the matching error_subcode 1487390. * feat(meta-ads): get_image_by_hash tool + docs steering LLMs to it (pipeboard-co#127) * docs(upload_ad_image): tell LLMs the response carries a viewable URL The Returns section only mentioned image_hash for create_ad_creative. Production logs show LLMs receiving a hash from upload_ad_image and then calling get_ad_image with (account_id, image_hash), getting a Pydantic validation error because that tool only accepts ad_id. The hash + URL were already in the upload response — the LLM had no reason to know. Expand the Returns section to surface the images[].url Meta CDN link, state outright that no get-by-hash tool exists, and clarify that get_ad_image is for images already attached to an ad. * feat(get_image_by_hash): new tool to view a Meta image by (account_id, image_hash) upload_ad_image already returns a Meta CDN url in the response, but image hashes also surface in get_creative_details, get_ad_creatives, and other read tools. Without a dedicated lookup tool, an LLM seeing a hash had no clean way to view the image short of attaching it to an ad first. Add get_image_by_hash(account_id, image_hash) that hits act_{account_id}/adimages directly and returns an Image. Extract the image-by-hash logic that get_ad_image already had into a shared _fetch_image_by_hash helper so both code paths use it. Update get_ad_image and upload_ad_image docstrings to mention the new tool. Tests cover the happy path, act_-prefix normalization, and the two empty-input branches. * chore(release): bump to 1.0.110 for get_image_by_hash * docs(readme): writes-first framing + subdomain URLs + add Snap and Reddit (pipeboard-co#128) * docs(readme): lead with Pipeboard MCP family (5 platforms, 230+ tools) (pipeboard-co#129) NO-DEPLOY Reframes README so single-platform comparisons cannot reduce Pipeboard to a Meta-only MCP. New top-level family section with verified tool counts (42/59/59/37/33 = 230+) and CLI placement. Removes redundant mid-document Also Available table. * Fix DOF + video + multi-text returning Meta error 2061015 (pipeboard-co#130) * Fix DOF + video + multi-text returning Meta error_subcode 2061015 For DOF + video, the canonical Meta shape requires link_urls + CTA in asset_feed_spec and a bare object_story_spec (no video_data anchor). The DOF branch in create_ad_creative was previously building a minimal AFS (DOF + image shape) regardless of media type, so DOF + video creatives had neither link_urls/CTA in AFS nor a link_data anchor in OSS, and Meta rejected the request with 2061015 (The link field is required). Fix: gate the minimal-AFS branch on DOF AND not video. DOF + video now falls into the non-DOF path that adds link_urls (when provided), ad_formats, optimization_type, and the CTA into AFS while keeping object_story_spec bare. Adding a video_data anchor would trip Meta error 1443048, so the bare OSS is intentional. Tests: - test_video_creative_with_dof_includes_link_and_cta_in_asset_feed_spec - test_video_creative_with_dof_and_call_now_cta_uses_call_to_actions_plural * Add link_data anchor on object_story_spec for DOF + video Empirical v24 sandbox testing shows Meta rejects a bare object_story_spec of {page_id} for DOF + video with error_subcode 2061015 ("link field required", blame_field_specs=[["link"]]). The prior fix removed 2061015 only when link_urls landed in asset_feed_spec, but a missing OSS anchor still trips the same subcode on some surfaces. Adding a minimal link_data: {link: <url>} on object_story_spec resolves it. A video_data anchor would trip 1443226 ("thumbnail required") instead, so the link variant is the only working shape. The link is sourced from link_url when provided, falling back to http://fb.me/ (Meta lead-gen placeholder) when lead_gen_form_id is present. Tests updated to expect the new OSS shape for DOF + video, including the CALL_NOW phone-number case. Verified end-to-end against the v24 sandbox 2026-05-27 via pipeboard mcp tools-call bulk_create_ad_creatives. * Bump version to 1.0.111 (pipeboard-co#131) * fix(video): surface real thumbnail frame and processing status (pipeboard-co#132) When a video has not finished transcoding, Meta returns a placeholder "picture" (size 228x119, stp=dst-jpg_p228x119) instead of a real frame. Using that placeholder in object_story_spec or asset_feed_spec yields a video ad with a tiny/blank poster in Ads Manager. This change: - Requests thumbnails + status alongside picture when fetching video metadata. - Prefers thumbnails.data[0].uri (a real frame URL) over picture when surfacing thumbnail_url to callers. - Surfaces video_status (processing|ready|error|expired) on get_ad_video responses and a warning_processing hint when the video is still transcoding so the LLM polls instead of using the placeholder. - create_ad_creative now blocks attempts to wrap a still-processing video without an explicit thumbnail_url, with a clear error pointing callers at get_ad_video for polling. * Bump version to 1.0.112 (pipeboard-co#133) * fix(auth): distinguish policy-blocked errors from token expiry (1.0.113) (pipeboard-co#134) * fix(auth): distinguish policy-blocked errors from token expiry (1.0.113) Code 368 (action disallowed) and code 190 + subcode 459 (user checkpointed by Facebook security) leave the access token valid; invalidating it and surfacing a renew-token message is misleading. - Skip auth_manager.invalidate_token() for those two cases. - Add is_account_disabled flag on the error payload with the original error_code and error_subcode so callers can branch on it instead of treating every auth failure as a stale token. - Other code 190 subcodes (458, 460, 463, 464, 467) remain genuine session errors and keep the existing invalidate + reauth path. * chore: bump server.json to 1.0.113 to match pyproject.toml * fix(deps): require starlette>=1.0.1 (CVE-2026-48710) and drop unused poetry.lock (pipeboard-co#136) starlette is pulled in transitively by mcp for the streamable-http transport. Versions <= 1.0.0 are affected by CVE-2026-48710 (BadHost), a Host-header validation bypass that can poison request.url.path and bypass path-based auth. Add an explicit starlette>=1.0.1 floor to pyproject.toml and requirements.txt; uv resolves the set to starlette 1.2.1. The stale poetry.lock pinned starlette 0.46.1 and an outdated mcp 1.6.0; nothing builds from it (Docker and CI install from requirements.txt via uv), so it is removed. This clears the Dependabot alert at its source. * fix(auth): require a primary access-token credential at the HTTP middleware (1.0.114) (pipeboard-co#137) * fix(auth): require a primary access-token credential at the HTTP middleware (1.0.114) Tighten AuthInjectionMiddleware so it admits a request only when a primary access-token credential is present (Authorization: Bearer, X-META-ACCESS-TOKEN, or X-PIPEBOARD-API-TOKEN). X-Pipeboard-Token is a supplementary service token used by the duplication callback and is not a standalone authentication credential. It continues to be forwarded alongside a primary credential, so the duplication flow is unaffected. Adds regression tests and bumps the version to 1.0.114. * chore: bump server.json to 1.0.114 to match pyproject.toml * fix(security): block SSRF in upload_ad_image image_url fetch (GHSA-45gf-fjxp-cjpq) (1.0.115) (pipeboard-co#138) upload_ad_image and the image-viewing tools fetched a caller-supplied URL server-side with follow_redirects enabled and no host/IP validation, allowing the server to reach loopback, private-network, and cloud-metadata addresses. - Add validate_public_url(): allow only http/https and reject hosts resolving to private/loopback/link-local/reserved/multicast/unspecified addresses (unwraps IPv4-mapped IPv6). - Re-validate every redirect hop via an httpx request event hook on all outbound image fetches. - download_image returns None for blocked URLs; try_multiple_download_methods raises so upload_ad_image surfaces a clear error. - Add tests (tests/test_ssrf_url_validation.py); document advisory in SECURITY.md; bump to 1.0.115. Fixes GHSA-45gf-fjxp-cjpq. * docs(security): document GHSA-2v2f-mvfg-ph56 in SECURITY.md (pipeboard-co#139) Add the X-Pipeboard-Token auth-bypass advisory entry (fixed in 1.0.115) alongside the existing GHSA-45gf and GHSA-9gw6 entries. Documentation only; the fix already shipped. * docs(create_adset): clarify OUTCOME_ENGAGEMENT conversion locations (pipeboard-co#140) * docs(create_adset): clarify OUTCOME_ENGAGEMENT conversion locations On Post engagement uses destination_type=ON_POST (not ON_AD) and needs promoted_object={page_id} set at creation (immutable; otherwise create_ad fails with subcode 1885154). ON_AD is the OUTCOME_LEADS instant-form destination and Meta rejects it for OUTCOME_ENGAGEMENT (subcode 1815715). Also list the ON_POST/ON_PAGE/ON_EVENT/ON_VIDEO destination_type values and document that the Ads Manager Profile and Page visits goal (PROFILE_AND_PAGE_ENGAGEMENT) is not available via the Marketing API for OUTCOME_ENGAGEMENT. Docstring only; no behavior change (destination_type still passes through to Meta). * chore(release): bump to 1.0.116 * fix(insights): keep leads/CPL on platform_position breakdown (pipeboard-co#141) * fix(insights): keep action data on platform_position breakdown (bump 1.0.117) get_insights with breakdown=platform_position was dropping the action-typed fields (actions, action_values, conversions, cost_per_action_type), so placement-level reports lost leads, CPL, and conversions while spend/clicks/impressions came through. Root cause: the Meta (pipeboard-co#100) "(action_type, platform_position) is invalid" error is caused solely by platform_position not being paired with publisher_platform. The tool already auto-adds publisher_platform; once paired, Meta returns the action-typed fields per placement (verified live against the Graph API with action_breakdowns default, [], and [action_type]). Stripping the fields was an unnecessary over-correction. Removes the field-stripping path and its constants; adds regression tests. * chore: sync server.json version to 1.0.117 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Yves Junqueira <yves.junqueira@gmail.com> Co-authored-by: George Phelipe Azevedo <george@pipeboard.co> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Yves Junqueira <202998+nictuku@users.noreply.github.com> Co-authored-by: Herriaan <mail@katama.nl>
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.
Adds the
GHSA-2v2f-mvfg-ph56(X-Pipeboard-Token auth bypass) advisory entry to SECURITY.md, alongside the existing GHSA-45gf and GHSA-9gw6 entries. Documents affected versions (<= 1.0.113), fixed-in (1.0.115), what went wrong, the fix, and operator actions (upgrade + rotate token if exposed).Documentation only — the fix already shipped in 1.0.115. No release needed.