zuckerbot-mcp 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -130,14 +130,14 @@ function buildQuickstartPayload(client) {
130
130
  {
131
131
  step: 4,
132
132
  tool: "zuckerbot_create_campaign",
133
- description: "Full campaign strategy with audience tiers",
133
+ description: "Create a reviewed launch-ready campaign draft",
134
134
  requires_key: true,
135
135
  flow: "quick_launch",
136
136
  },
137
137
  {
138
138
  step: 5,
139
139
  tool: "zuckerbot_launch_campaign",
140
- description: "Go live on Meta",
140
+ description: "Launch the reviewed legacy draft on Meta",
141
141
  requires_key: true,
142
142
  flow: "quick_launch",
143
143
  },
@@ -367,7 +367,7 @@ function registerCreativeGenerationTools(server, client) {
367
367
  if (creative_handoff)
368
368
  body.creative_handoff = creative_handoff;
369
369
  const result = await client.post(`/campaigns/${campaign_id}/request-creative`, body);
370
- return formatResult(appendHint(result, "Creative request dispatched. Monitor progress with zuckerbot_get_creative_status. Once all_complete is true, call zuckerbot_activate_campaign to go live."));
370
+ return formatResult(appendHint(result, "Creative request dispatched for planning/review. Monitor progress with zuckerbot_get_creative_status. Multi-tier activation is temporarily disabled; use a legacy campaign draft for a live launch."));
371
371
  }
372
372
  catch (err) {
373
373
  return formatError(err);
@@ -433,26 +433,53 @@ export function registerTools(server, client) {
433
433
  registerQuickstartTool(server, client);
434
434
  registerBillingStatusTool(server, client);
435
435
  // ── 0a. Account Audit ───────────────────────────────────────────
436
- server.tool("zuckerbot_audit_account", "Run a full audit of the connected Meta ad account: wasted spend detection, creative fatigue, opportunity score (0-100), projected CPL improvement, and prioritised action items. Read-only and available on every tier — the recommended FIRST call for any new account or when a user asks 'how are my ads doing?'.", {
436
+ server.tool("zuckerbot_audit_account", "Run a full audit of the connected Meta ad account: wasted spend detection, creative fatigue, a complete-account opportunity score (0-100 when all inputs return), and prioritised action items. Saves a shareable web report when the API key resolves to one saved business. Read-only and available on every tier — the recommended FIRST call for any new account or when a user asks 'how are my ads doing?'.", {
437
437
  meta_ad_account_id: z.string().optional().describe("Meta ad account ID to audit (format: act_XXXXX). Defaults to the connected account."),
438
438
  company_name: z.string().optional().describe("Company name used in the audit narrative. Defaults to the connected business name."),
439
439
  }, async ({ meta_ad_account_id, company_name }) => {
440
440
  try {
441
441
  client.requireAuth();
442
- const body = {};
442
+ // Ask the API to persist when one business is resolvable, but keep the
443
+ // audit itself available when setup/selection is still incomplete.
444
+ // This stays one metered request and one Meta audit in both cases.
445
+ const body = {
446
+ save_report: true,
447
+ save_report_if_available: true,
448
+ };
443
449
  if (meta_ad_account_id)
444
450
  body.meta_ad_account_id = meta_ad_account_id;
445
451
  if (company_name)
446
452
  body.company_name = company_name;
447
453
  const result = await client.post("/audit", body);
448
- return formatResult(appendHint(result, "Audit complete. Present opportunityScore, wastedSpendEstCents, and audit.rawInsights.action_items to the user, then act on the findings: zuckerbot_get_performance to drill into a specific campaign, zuckerbot_pause_campaign to stop wasted spend, or zuckerbot_creative_analysis to diagnose fatigued creatives."));
454
+ const record = asRecord(result);
455
+ const audit = asRecord(record?.audit);
456
+ const raw = asRecord(audit?.rawInsights);
457
+ const dataCompleteness = asRecord(raw?.data_completeness);
458
+ // Raw Meta insight rows drown the agent's context; the summarised
459
+ // findings above them carry everything the report presents.
460
+ if (raw && "campaign_rows" in raw)
461
+ delete raw.campaign_rows;
462
+ const reportUrl = typeof record?.report_url === "string" ? `https://zuckerbot.ai${record.report_url}` : null;
463
+ const partialDataHint = raw?.data_truncated === true
464
+ ? " NOTE: Meta returned incomplete data. Check audit.rawInsights.data_completeness and do not present opportunityScore or exact spend/fatigue ratios as complete-account measures."
465
+ : "";
466
+ const scoreHint = typeof audit?.opportunityScore !== "number"
467
+ ? " NOTE: No opportunity score is available for this result. Do not present or infer a numeric account rating."
468
+ : "";
469
+ const currencyHint = dataCompleteness?.account_currency_available === false
470
+ ? " NOTE: Meta did not return the account currency. Describe money as account-currency units; do not label it AUD or invent another currency code."
471
+ : "";
472
+ const reportHint = reportUrl
473
+ ? ` A shareable web report was saved — give the user this link: ${reportUrl}`
474
+ : " No shareable report was saved because this API key does not resolve to one saved business.";
475
+ return formatResult(appendHint(result, `Audit complete.${reportHint}${partialDataHint}${scoreHint}${currencyHint} Present complete observed findings and audit.rawInsights.action_items to the user, then act on them: zuckerbot_get_performance to drill into a specific campaign, zuckerbot_pause_campaign to stop wasted spend, or zuckerbot_creative_analysis to diagnose fatigued creatives.`));
449
476
  }
450
477
  catch (err) {
451
478
  return formatError(err);
452
479
  }
453
480
  });
454
481
  // ── 0b. Redeem Lifetime Licence ─────────────────────────────────
455
- server.tool("zuckerbot_redeem_license", "Redeem a ZuckerBot lifetime licence code (format ZB-XXXXX-XXXXX-XXXXX) purchased on Dealify or AppSumo. Codes stack on one account: 1 code = Lifetime Tier 1 (1 ad account, 2,500 calls/mo), 2 codes = Tier 2 (3 accounts, 10K calls/mo), 3 codes = Tier 3 (10 accounts, 30K calls/mo). Redeeming upgrades ALL of the account's API keys to the new tier immediately.", {
482
+ server.tool("zuckerbot_redeem_license", "Redeem a ZuckerBot lifetime licence code (format ZB-XXXXX-XXXXX-XXXXX) purchased on Dealify or AppSumo. Each code activates the plan it was purchased for: Tier 1 (1 ad account, 2,500 calls/mo), Tier 2 (3 accounts, 10K calls/mo) or Tier 3 (10 accounts, 30K calls/mo). Codes also stack additively on one account up to Tier 3 — e.g. two Tier 1 codes = Tier 2. Redeeming upgrades ALL of the account's API keys to the new tier immediately.", {
456
483
  code: z.string().describe("Lifetime licence code in the format ZB-XXXXX-XXXXX-XXXXX"),
457
484
  }, async ({ code }) => {
458
485
  try {
@@ -462,7 +489,7 @@ export function registerTools(server, client) {
462
489
  const mintedKey = asRecord(record?.api_key);
463
490
  const hint = mintedKey
464
491
  ? "IMPORTANT: a new API key was minted during redemption. Show the full api_key.key value to the user IMMEDIATELY and tell them to store it securely — it will NEVER be shown again. Then follow the next_steps in the response."
465
- : "Licence redeemed — all of this account's API keys are upgraded to the new tier. Compare codes_redeemed with max_stack: stacking another code unlocks the next tier. Run zuckerbot_audit_account to put the new limits to work.";
492
+ : "Licence redeemed — all of this account's API keys are upgraded to the new tier. If the tier is below Lifetime Tier 3, stacking another code raises it. Run zuckerbot_audit_account to put the new limits to work.";
466
493
  return formatResult(appendHint(result, hint));
467
494
  }
468
495
  catch (err) {
@@ -608,15 +635,15 @@ export function registerTools(server, client) {
608
635
  }
609
636
  });
610
637
  // ── 7. Create Full Campaign ─────────────────────────────────────
611
- server.tool("zuckerbot_create_full_campaign", "Build a complete Meta campaign from an approved Campaign Architect session. IMPORTANT: dry_run defaults to TRUE. When dry_run=true, returns the exact campaign structure that WOULD be created without calling Meta — safe, free, no side effects. Present this to the customer first. Only set dry_run=false after explicit customer approval. When dry_run=false, creates live Meta objects (campaign + ad sets + ads) in PAUSED state. Generated videos get linked for rejection tracking automatically.", {
638
+ server.tool("zuckerbot_create_full_campaign", "Build a complete PAUSED Meta campaign from an approved Campaign Architect session. IMPORTANT: dry_run defaults to TRUE. When dry_run=true, returns the exact campaign structure that WOULD be created without calling Meta — safe, free, no side effects. Present this to the customer first. Only set dry_run=false after explicit customer approval. When dry_run=false, creates Meta objects in PAUSED state for review; Architect auto-activation is temporarily disabled. Generated videos get linked for rejection tracking automatically.", {
612
639
  session_id: z.string().describe("Campaign Architect session ID with approved strategy and creatives"),
613
640
  dry_run: z.boolean().default(true)
614
641
  .describe("DEFAULT TRUE. Set to false ONLY after customer has approved the dry-run preview. Live mode creates real Meta objects."),
615
642
  meta_access_token: z.string().optional().describe("Meta access token override (live mode only)"),
616
643
  meta_ad_account_id: z.string().optional().describe("Meta ad account ID override (live mode only)"),
617
644
  meta_page_id: z.string().optional().describe("Facebook Page ID override (live mode only)"),
618
- activate: z.boolean().default(false)
619
- .describe("If true AND dry_run=false, activate immediately after creation. Default false stays PAUSED for customer approval."),
645
+ activate: z.literal(false).default(false)
646
+ .describe("Must remain false. Architect auto-activation is temporarily disabled; live mode creates PAUSED Meta objects only."),
620
647
  }, async ({ session_id, dry_run, meta_access_token, meta_ad_account_id, meta_page_id, activate }) => {
621
648
  try {
622
649
  client.requireAuth();
@@ -632,9 +659,7 @@ export function registerTools(server, client) {
632
659
  const isDryRun = record.dry_run === true;
633
660
  const hint = isDryRun
634
661
  ? "DRY RUN — no Meta objects were created. Present the would_create structure to the customer. If they approve, call again with dry_run=false to create live Meta objects."
635
- : activate
636
- ? "Campaign built and activated on Meta. Monitor performance with zuckerbot_get_performance. Generated videos are linked for rejection tracking."
637
- : "Campaign built in PAUSED state on Meta. Present the meta_structure to the customer, then resume the campaign on Meta only after approval. Videos linked for rejection tracking.";
662
+ : "Campaign built in PAUSED state on Meta for review. Architect auto-activation and campaign resume are temporarily disabled; use a reviewed legacy draft with zuckerbot_launch_campaign for the supported live path. Videos linked for rejection tracking.";
638
663
  return formatResult(appendHint(result, hint));
639
664
  }
640
665
  catch (err) {
@@ -657,14 +682,14 @@ export function registerTools(server, client) {
657
682
  url,
658
683
  ad_count,
659
684
  });
660
- return formatResult(appendHint(result, "Preview generated. Call zuckerbot_create_campaign with the same URL to build a full intelligence strategy with audience tiers and budget guidance."));
685
+ return formatResult(appendHint(result, "Preview generated. Call zuckerbot_create_campaign with the same URL; it defaults to the reviewed legacy draft path that can be launched safely."));
661
686
  }
662
687
  catch (err) {
663
688
  return formatError(err);
664
689
  }
665
690
  });
666
691
  // ── 7. Create Campaign ──────────────────────────────────────────
667
- server.tool("zuckerbot_create_campaign", "Create a new campaign draft for a business. In intelligence mode (preferred when a business_id is available) this returns context-aware audience tiers, creative angles, budget guidance, projected CPL, and phased next steps. Does NOT spend money or create anything on Meta it's purely a planning step. Use zuckerbot_launch_campaign to go live after strategy approval.", {
692
+ server.tool("zuckerbot_create_campaign", "Create a new campaign draft for a business. Defaults to legacy mode, the only launch-ready path during Dealify hardening. Intelligence mode remains available for planning only and cannot be activated. This tool does not spend money or create anything on Meta; review the draft, then use zuckerbot_launch_campaign.", {
668
693
  url: z.string().describe("Business website URL"),
669
694
  business_id: z.string().optional().describe("Existing ZuckerBot business ID to anchor intelligence mode"),
670
695
  architect_session_id: z.string().optional()
@@ -682,8 +707,8 @@ export function registerTools(server, client) {
682
707
  .describe("Daily budget in cents (e.g., 2000 = $20/day)"),
683
708
  mode: z
684
709
  .enum(["auto", "legacy", "intelligence"])
685
- .optional()
686
- .describe("Campaign planning mode. auto prefers intelligence when a business can be resolved"),
710
+ .default("legacy")
711
+ .describe("Campaign planning mode. Default legacy is the only launch-ready path. auto/intelligence are planning-only while multi-tier activation is disabled."),
687
712
  objective: z
688
713
  .enum(["leads", "traffic", "conversions", "awareness"])
689
714
  .optional()
@@ -713,8 +738,7 @@ export function registerTools(server, client) {
713
738
  body.location = location;
714
739
  if (budget_daily_cents !== undefined)
715
740
  body.budget_daily_cents = budget_daily_cents;
716
- if (mode)
717
- body.mode = mode;
741
+ body.mode = mode ?? "legacy";
718
742
  if (objective)
719
743
  body.objective = objective;
720
744
  if (lead_destination)
@@ -726,7 +750,9 @@ export function registerTools(server, client) {
726
750
  if (creative_handoff)
727
751
  body.creative_handoff = creative_handoff;
728
752
  const result = await client.post("/campaigns/create", body);
729
- return formatResult(appendHint(result, "Campaign draft created. For intelligence campaigns: call zuckerbot_approve_campaign_strategy to confirm audience tiers and angles, then zuckerbot_request_creative or zuckerbot_upload_creative to attach creatives before launch."));
753
+ return formatResult(appendHint(result, mode === "legacy"
754
+ ? "Launch-ready legacy draft created. Review it, confirm Meta credentials and budget, then call zuckerbot_launch_campaign."
755
+ : "Planning-only intelligence draft created. Multi-tier activation is temporarily disabled; create a legacy-mode draft when the user is ready to launch."));
730
756
  }
731
757
  catch (err) {
732
758
  return formatError(err);
@@ -789,7 +815,7 @@ export function registerTools(server, client) {
789
815
  }, async ({ campaign_id }) => {
790
816
  try {
791
817
  const result = await client.get(`/campaigns/${campaign_id}`);
792
- return formatResult(appendHint(result, "Inspect creative_status and workflow state. Next steps depend on campaign_version: intelligence campaigns approve strategy upload creatives activate. Legacy campaigns → launch directly."));
818
+ return formatResult(appendHint(result, "Inspect creative_status and workflow state. Legacy campaigns can launch directly after review. Intelligence campaigns are planning-only while multi-tier activation is disabled."));
793
819
  }
794
820
  catch (err) {
795
821
  return formatError(err);
@@ -833,7 +859,7 @@ export function registerTools(server, client) {
833
859
  const creativeStatus = typeof payload?.creative_status === "string" ? payload.creative_status : null;
834
860
  const queuedJobs = Array.isArray(payload?.queued_jobs) ? payload.queued_jobs : [];
835
861
  if (creativeStatus !== "uploading" || queuedJobs.length === 0) {
836
- return formatResult(appendHint(result, "Creatives uploaded. Call zuckerbot_get_creative_status to check processing status, then zuckerbot_activate_campaign once all_complete is true."));
862
+ return formatResult(appendHint(result, "Creatives uploaded. Call zuckerbot_get_creative_status to check processing status. Intelligence activation is temporarily disabled; use a reviewed legacy draft for live launch."));
837
863
  }
838
864
  try {
839
865
  const polled = await pollCreativeStatus(client, campaign_id);
@@ -847,7 +873,7 @@ export function registerTools(server, client) {
847
873
  interval_seconds: CREATIVE_STATUS_POLL_INTERVAL_MS / 1000,
848
874
  },
849
875
  final_status: polled.lastStatus,
850
- }, "All creatives uploaded and processed. Call zuckerbot_activate_campaign to turn on the paused Meta ad sets and go live."));
876
+ }, "All creatives uploaded and processed for review. Intelligence activation is temporarily disabled; use a reviewed legacy draft for live launch."));
851
877
  }
852
878
  return formatResult(appendHint({
853
879
  initial_response: payload,
@@ -860,7 +886,7 @@ export function registerTools(server, client) {
860
886
  suggested_tool: "zuckerbot_get_creative_status",
861
887
  },
862
888
  last_status: polled.lastStatus,
863
- }, "Uploads still processing. Call zuckerbot_get_creative_status in 15–30 seconds to check progress. Once all_complete is true, call zuckerbot_activate_campaign."));
889
+ }, "Uploads still processing. Call zuckerbot_get_creative_status in 15–30 seconds to check progress. Completed intelligence creatives remain review-only while activation is disabled."));
864
890
  }
865
891
  catch (pollError) {
866
892
  return formatResult(appendHint({
@@ -876,40 +902,36 @@ export function registerTools(server, client) {
876
902
  return formatError(err);
877
903
  }
878
904
  });
879
- server.tool("zuckerbot_get_creative_status", "Check the asynchronous upload queue for an intelligence campaign to see if Meta ad-creation jobs are complete. Poll this after zuckerbot_upload_creative when the initial response shows creative_status='uploading'. Returns all_complete=true when every queued job has finished and the campaign is ready to activate.", {
905
+ server.tool("zuckerbot_get_creative_status", "Check the asynchronous upload queue for an intelligence campaign to see if Meta ad-creation jobs are complete. Poll this after zuckerbot_upload_creative when the initial response shows creative_status='uploading'. Returns all_complete=true when every queued job has finished for review; intelligence activation is temporarily disabled.", {
880
906
  campaign_id: z.string().describe("Intelligence campaign ID"),
881
907
  }, async ({ campaign_id }) => {
882
908
  try {
883
909
  const result = await client.get(`/campaigns/${campaign_id}/creative-status`);
884
- return formatResult(appendHint(result, "If all_complete is true, call zuckerbot_activate_campaign to turn on the paused Meta ad sets. If still uploading, poll again in 15–30 seconds."));
910
+ return formatResult(appendHint(result, "If still uploading, poll again in 15–30 seconds. Completed intelligence creatives remain available for review, but multi-tier activation is temporarily disabled; use a legacy draft for live launch."));
885
911
  }
886
912
  catch (err) {
887
913
  return formatError(err);
888
914
  }
889
915
  });
890
- server.tool("zuckerbot_activate_campaign", "Activate the ready audience tiers for an intelligence campaign after strategy approval and creative upload. Turns on paused Meta ad sets for each approved tier. Only tiers with valid paused executions and uploaded creatives are activated. Use this as the final step before a campaign goes live on Meta.", {
916
+ server.tool("zuckerbot_activate_campaign", "Temporarily unavailable during Dealify launch hardening. Intelligence campaigns are planning-only; create a legacy-mode draft and use zuckerbot_launch_campaign for the supported live path.", {
891
917
  campaign_id: z.string().describe("Intelligence campaign ID"),
892
918
  tier_names: z.array(z.string()).optional().describe("Optional subset of approved tiers to activate"),
893
919
  meta_access_token: z.string().optional().describe("Optional Meta/Facebook access token override"),
894
920
  meta_ad_account_id: z.string().optional().describe("Optional Meta ad account ID override (format: act_XXXXX)"),
895
921
  meta_page_id: z.string().optional().describe("Optional Facebook Page ID override"),
896
922
  }, async ({ campaign_id, tier_names, meta_access_token, meta_ad_account_id, meta_page_id }) => {
897
- try {
898
- const body = {};
899
- if (tier_names?.length)
900
- body.tier_names = tier_names;
901
- if (meta_access_token)
902
- body.meta_access_token = meta_access_token;
903
- if (meta_ad_account_id)
904
- body.meta_ad_account_id = meta_ad_account_id;
905
- if (meta_page_id)
906
- body.meta_page_id = meta_page_id;
907
- const result = await client.post(`/campaigns/${campaign_id}/activate`, body);
908
- return formatResult(appendHint(result, "Campaign activated on Meta. Monitor with zuckerbot_get_performance. If budget needs adjusting, use zuckerbot_pause_campaign to stop spend, or zuckerbot_rebalance_portfolio if this campaign belongs to a portfolio."));
909
- }
910
- catch (err) {
911
- return formatError(err);
912
- }
923
+ void tier_names;
924
+ void meta_access_token;
925
+ void meta_ad_account_id;
926
+ void meta_page_id;
927
+ return formatResult({
928
+ error: true,
929
+ code: "intelligence_activation_temporarily_disabled",
930
+ message: "Multi-tier activation is temporarily disabled. Create a campaign with mode='legacy', review it, then call zuckerbot_launch_campaign.",
931
+ campaign_id,
932
+ supported_create_mode: "legacy",
933
+ supported_launch_tool: "zuckerbot_launch_campaign",
934
+ });
913
935
  });
914
936
  server.tool("zuckerbot_suggest_angles", "Return only the creative angles and audience tiers for a campaign draft — a lightweight alternative to zuckerbot_get_campaign when you need just the strategy summary without the full campaign payload, stored creatives, or tier execution details.", {
915
937
  campaign_id: z.string().describe("Campaign ID"),
@@ -976,19 +998,19 @@ export function registerTools(server, client) {
976
998
  return formatError(err);
977
999
  }
978
1000
  });
979
- // ── 4. Pause / Resume Campaign ─────────────────────────────────
980
- server.tool("zuckerbot_pause_campaign", "Pause or resume a running Meta ad campaign. Pausing stops ad delivery and spend immediately the campaign stays in Meta but goes dark. Resuming restarts delivery without relaunching. Use pause when creative needs refreshing, budget is exhausted, or the user asks to stop spending.", {
1001
+ // ── 4. Pause Campaign ──────────────────────────────────────────
1002
+ server.tool("zuckerbot_pause_campaign", "Pause a running Meta ad campaign. Pausing stops ad delivery and spend immediately while leaving the campaign in Meta. Use this when creative needs refreshing, budget is exhausted, entitlement is withdrawn, or the user asks to stop spending. Resume is temporarily disabled during Dealify launch hardening.", {
981
1003
  campaign_id: z.string().describe("ZuckerBot campaign ID"),
982
1004
  action: z
983
- .enum(["pause", "resume"])
1005
+ .literal("pause")
984
1006
  .default("pause")
985
- .describe("Whether to pause or resume the campaign"),
1007
+ .describe("Pause the campaign"),
986
1008
  }, async ({ campaign_id, action }) => {
987
1009
  try {
988
1010
  const result = await client.post(`/campaigns/${campaign_id}/pause`, {
989
1011
  action,
990
1012
  });
991
- return formatResult(appendHint(result, "Campaign paused/resumed. Call zuckerbot_get_performance to see the current delivery status, or zuckerbot_get_campaign to check the full workflow state."));
1013
+ return formatResult(appendHint(result, "Campaign paused. Call zuckerbot_get_performance to confirm delivery has stopped, or zuckerbot_get_campaign to inspect the full workflow state."));
992
1014
  }
993
1015
  catch (err) {
994
1016
  return formatError(err);
@@ -1253,7 +1275,7 @@ export function registerTools(server, client) {
1253
1275
  return formatError(err);
1254
1276
  }
1255
1277
  });
1256
- server.tool("zuckerbot_get_campaign_insights", "Query campaign, ad set, or ad-level performance for any campaign in the connected Meta ad account — including campaigns not created by ZuckerBot. Tags each row with is_zuckerbot so you can benchmark ZuckerBot campaigns against manually managed ones. Supports date range filtering, campaign name search, status filters, time-series breakdowns, and multi-column sorting. For deduped truth use meta_result / cost_per_meta_result (Meta's Ads Manager \"Results\" for any objective) or conversion_leads / meta_leads — not the inflated, deprecated leads / cpl. meta_result and conversion_leads are most reliable on campaign-level, non-time-incremented queries.", {
1278
+ server.tool("zuckerbot_get_campaign_insights", "Query campaign, ad set, or ad-level performance for any campaign in the connected Meta ad account — including campaigns not created by ZuckerBot. Tags each row with is_zuckerbot so you can benchmark ZuckerBot campaigns against manually managed ones. Supports date range filtering, campaign name search, status filters, time-series breakdowns, and multi-column sorting. For deduped truth use meta_result / cost_per_meta_result (Meta's Ads Manager \"Results\" for any objective) or conversion_leads / meta_leads — not the inflated, deprecated leads / cpl (the response's metric_semantics object labels every lead field). meta_result and conversion_leads are most reliable on campaign-level, non-time-incremented queries. Adset/ad rows report their OWN status — status_scope says which entity a row's status belongs to, statuses are as of status_synced_at, and refresh=true re-syncs them; spend-by-date is authoritative for whether delivery actually happened.", {
1257
1279
  business_id: z.string().optional().describe("Optional business ID override linked to the connected Meta ad account"),
1258
1280
  date_from: z.string().optional().describe("Optional start date in YYYY-MM-DD format. Defaults to 7 days ago."),
1259
1281
  date_to: z.string().optional().describe("Optional end date in YYYY-MM-DD format. Defaults to today."),
@@ -1319,7 +1341,10 @@ export function registerTools(server, client) {
1319
1341
  })
1320
1342
  .optional()
1321
1343
  .describe("Optional user data to improve match rate"),
1322
- }, async ({ campaign_id, lead_id, quality, meta_access_token, user_data }) => {
1344
+ fbclid: z.string().optional().describe("Optional Facebook click ID; the server builds a well-formed fbc cookie from it"),
1345
+ fbc: z.string().optional().describe("Optional pre-formatted fbc cookie, forwarded raw — never hashed"),
1346
+ fbp: z.string().optional().describe("Optional _fbp browser cookie, forwarded raw — never hashed. Improves match quality"),
1347
+ }, async ({ campaign_id, lead_id, quality, meta_access_token, user_data, fbclid, fbc, fbp }) => {
1323
1348
  try {
1324
1349
  const body = {
1325
1350
  lead_id,
@@ -1328,6 +1353,12 @@ export function registerTools(server, client) {
1328
1353
  };
1329
1354
  if (user_data)
1330
1355
  body.user_data = user_data;
1356
+ if (fbclid)
1357
+ body.fbclid = fbclid;
1358
+ if (fbc)
1359
+ body.fbc = fbc;
1360
+ if (fbp)
1361
+ body.fbp = fbp;
1331
1362
  const result = await client.post(`/campaigns/${campaign_id}/conversions`, body);
1332
1363
  return formatResult(appendHint(result, "Conversion signal sent. Use zuckerbot_capi_status to view aggregate CAPI delivery and attribution, or zuckerbot_get_performance to see updated CPL and lead metrics."));
1333
1364
  }
@@ -1472,7 +1503,7 @@ export function registerTools(server, client) {
1472
1503
  }
1473
1504
  });
1474
1505
  // ── 18. CAPI Config — Read ─────────────────────────────────────
1475
- server.tool("zuckerbot_get_capi_config", "Fetch the current Conversions API configuration for a business: whether CAPI delivery is enabled, CRM source, currency, stage-to-event mappings, action source, and webhook URL. Use this before configuring CAPI to see what is already set, or to audit the current event mapping.", {
1506
+ server.tool("zuckerbot_get_capi_config", "Fetch the current Conversions API configuration for a business: whether CAPI delivery is enabled, CRM source, currency, stage-to-event mappings, action source, and webhook URL. Use this before configuring CAPI to see what is already set, or to audit the current event mapping. The webhook secret is write-only: reads return webhook_secret_set and webhook_secret_last4, never the full value — use zuckerbot_rotate_webhook_secret to mint a new one.", {
1476
1507
  business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
1477
1508
  }, async ({ business_id }) => {
1478
1509
  try {
@@ -1496,14 +1527,14 @@ export function registerTools(server, client) {
1496
1527
  .enum(["website", "app", "email", "phone_call", "chat", "physical_store", "system_generated", "business_messaging", "other"])
1497
1528
  .optional()
1498
1529
  .describe("Meta Conversions API action_source. Defaults to website for CRM events"),
1499
- rotate_webhook_secret: z.boolean().optional().describe("Rotate the webhook secret on update"),
1530
+ rotate_webhook_secret: z.boolean().optional().describe("Rotate the webhook secret on update. The new secret is returned exactly once in the response; prefer zuckerbot_rotate_webhook_secret for a dedicated rotation"),
1500
1531
  event_mapping: z
1501
1532
  .record(z.string(), z.object({
1502
- meta_event: z.string().describe("Meta standard event name"),
1533
+ meta_event: z.string().describe("Meta event name: standard names (Lead, Purchase, StartTrial, …) are canonicalised case-insensitively; custom names (e.g. plg_signup_completed) are stored verbatim — start with a letter, letters/digits/underscore/hyphen/period/space only, max 50 chars"),
1503
1534
  value: z.number().optional().describe("Event value in major currency units"),
1504
1535
  }))
1505
1536
  .optional()
1506
- .describe("CRM stage mapping object keyed by source stage"),
1537
+ .describe("CRM stage mapping object keyed by source stage. Stage keys are normalised (lower-cased, non-alphanumerics stripped: signup_completed → signupcompleted); inbound webhook source_stage values are normalised identically before matching, and any renamed keys are reported back as normalised_keys in the response"),
1507
1538
  }, async ({ business_id, is_enabled, currency, crm_source, optimise_for, action_source, rotate_webhook_secret, event_mapping }) => {
1508
1539
  try {
1509
1540
  const resolvedBusinessId = await client.resolveBusinessId(business_id);
@@ -1529,6 +1560,19 @@ export function registerTools(server, client) {
1529
1560
  return formatError(err);
1530
1561
  }
1531
1562
  });
1563
+ // ── 19b. CAPI Webhook Secret — Rotate ──────────────────────────
1564
+ server.tool("zuckerbot_rotate_webhook_secret", "Rotate the Conversions API webhook secret for a business. The new secret is returned exactly once, in this response only — every other read shows just webhook_secret_set and webhook_secret_last4. The old secret stops authenticating immediately, so update the system that signs your inbound webhooks (for example your CRM workflow's stored secret) in the same sitting.", {
1565
+ business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
1566
+ }, async ({ business_id }) => {
1567
+ try {
1568
+ const resolvedBusinessId = await client.resolveBusinessId(business_id);
1569
+ const result = await client.put("/capi/config", { business_id: resolvedBusinessId, rotate_webhook_secret: true });
1570
+ return formatResult(appendHint(result, "Copy webhook_secret from this response now — it is not shown again. The old secret is already invalid, so update your webhook sender's stored secret immediately, then call zuckerbot_capi_test to verify delivery."));
1571
+ }
1572
+ catch (err) {
1573
+ return formatError(err);
1574
+ }
1575
+ });
1532
1576
  // ── 20. CAPI Status ────────────────────────────────────────────
1533
1577
  server.tool("zuckerbot_capi_status", "Get 7-day and 30-day CAPI delivery statistics for the business: total events sent, events by type (Lead/Contact/Purchase), match quality breakdown, and attribution counts. Use this to confirm CAPI is functioning and that events are being matched by Meta.", {
1534
1578
  business_id: z.string().optional().describe("Optional business ID override"),
@@ -1558,7 +1602,10 @@ export function registerTools(server, client) {
1558
1602
  })
1559
1603
  .optional()
1560
1604
  .describe("Optional user data to hash into the test payload"),
1561
- }, async ({ business_id, source_stage, crm_source, value, user_data }) => {
1605
+ fbclid: z.string().optional().describe("Optional Facebook click ID to verify fbc construction/passthrough"),
1606
+ fbc: z.string().optional().describe("Optional pre-formatted fbc cookie to verify raw passthrough"),
1607
+ fbp: z.string().optional().describe("Optional _fbp cookie to verify raw passthrough"),
1608
+ }, async ({ business_id, source_stage, crm_source, value, user_data, fbclid, fbc, fbp }) => {
1562
1609
  try {
1563
1610
  const resolvedBusinessId = await client.resolveBusinessId(business_id);
1564
1611
  const body = {
@@ -1572,6 +1619,12 @@ export function registerTools(server, client) {
1572
1619
  body.value = value;
1573
1620
  if (user_data)
1574
1621
  body.user_data = user_data;
1622
+ if (fbclid)
1623
+ body.fbclid = fbclid;
1624
+ if (fbc)
1625
+ body.fbc = fbc;
1626
+ if (fbp)
1627
+ body.fbp = fbp;
1575
1628
  const result = await client.post("/capi/config/test", body);
1576
1629
  return formatResult(appendHint(result, "Test event sent. Check status in the response — if 'sent', CAPI is working. If 'skipped' or 'failed', review the config with zuckerbot_get_capi_config and fix the event mapping."));
1577
1630
  }
@@ -1580,7 +1633,7 @@ export function registerTools(server, client) {
1580
1633
  }
1581
1634
  });
1582
1635
  // ── 22. Create Portfolio ───────────────────────────────────────
1583
- server.tool("zuckerbot_create_portfolio", "Create a multi-tier audience portfolio for a business from a shared template (e.g., 'Local Services', 'eCommerce') or a custom tier array. Portfolios split the total budget across prospecting, retargeting, and reactivation tiers with per-tier CPA targets. After creating, call zuckerbot_launch_portfolio to launch all tiers on Meta.", {
1636
+ server.tool("zuckerbot_create_portfolio", "Create a planning and monitoring-only multi-tier audience portfolio for a business from a shared template (e.g., 'Local Services', 'eCommerce') or a custom tier array. Portfolios split a proposed total budget across prospecting, retargeting, and reactivation tiers with per-tier CPA targets. Portfolio launch is temporarily disabled during Dealify hardening.", {
1584
1637
  business_id: z.string().optional().describe("Optional business ID override"),
1585
1638
  template_id: z.string().optional().describe("Optional portfolio template ID"),
1586
1639
  template_name: z.string().optional().describe("Optional portfolio template name, such as 'Local Services'"),
@@ -1615,19 +1668,19 @@ export function registerTools(server, client) {
1615
1668
  if (tiers)
1616
1669
  body.tiers = tiers;
1617
1670
  const result = await client.post("/portfolios/create", body);
1618
- return formatResult(appendHint(result, "Portfolio created. Call zuckerbot_launch_portfolio with the returned portfolio_id to launch all tiers on Meta, or zuckerbot_get_portfolio to review the tier configuration."));
1671
+ return formatResult(appendHint(result, "Planning portfolio created. Call zuckerbot_get_portfolio to review the tier configuration. Portfolio launch is temporarily disabled; use a reviewed legacy draft for live launch."));
1619
1672
  }
1620
1673
  catch (err) {
1621
1674
  return formatError(err);
1622
1675
  }
1623
1676
  });
1624
1677
  // ── 23. Get Portfolio ─────────────────────────────────────────
1625
- server.tool("zuckerbot_get_portfolio", "Fetch the configuration and current performance snapshot for an audience portfolio by ID. Returns tier definitions, budget allocations, CPA targets, and live performance data. Use this to inspect a portfolio before launching or rebalancing.", {
1678
+ server.tool("zuckerbot_get_portfolio", "Fetch the configuration and current performance snapshot for an audience portfolio by ID. Returns tier definitions, budget allocations, CPA targets, and any existing performance data. Use this to inspect a portfolio for planning, monitoring, or rebalancing an already-active portfolio.", {
1626
1679
  portfolio_id: z.string().describe("Audience portfolio ID"),
1627
1680
  }, async ({ portfolio_id }) => {
1628
1681
  try {
1629
1682
  const result = await client.get(`/portfolios/${portfolio_id}`);
1630
- return formatResult(appendHint(result, "Review tier config and performance. To adjust settings, call zuckerbot_update_portfolio. To launch on Meta, call zuckerbot_launch_portfolio. To rebalance active tier budgets, call zuckerbot_rebalance_portfolio."));
1683
+ return formatResult(appendHint(result, "Review tier config and performance. To adjust planning settings, call zuckerbot_update_portfolio. Existing active portfolios can be monitored or rebalanced, but new portfolio launch is temporarily disabled."));
1631
1684
  }
1632
1685
  catch (err) {
1633
1686
  return formatError(err);
@@ -1696,26 +1749,23 @@ export function registerTools(server, client) {
1696
1749
  }
1697
1750
  });
1698
1751
  // ── 27. Launch Portfolio ──────────────────────────────────────
1699
- server.tool("zuckerbot_launch_portfolio", "Launch all tiers in an audience portfolio on Meta. Creates separate Meta campaigns for each tier (prospecting, retargeting, reactivation) using the portfolio's budget allocations and CPA targets. Stored Meta credentials are auto-resolved. Tiers that lack required data (e.g., retargeting without a pixel, lookalike without enough seed events) are skipped with a reason.", {
1752
+ server.tool("zuckerbot_launch_portfolio", "Temporarily unavailable during Dealify launch hardening. Portfolio planning and monitoring remain available, but new multi-tier launches must not create Meta objects. Create a legacy-mode draft and use zuckerbot_launch_campaign for the supported live path.", {
1700
1753
  portfolio_id: z.string().describe("Audience portfolio ID to launch"),
1701
1754
  meta_access_token: z.string().optional().describe("Optional Meta/Facebook access token override"),
1702
1755
  meta_ad_account_id: z.string().optional().describe("Optional Meta ad account ID override (format: act_XXXXX)"),
1703
1756
  meta_page_id: z.string().optional().describe("Optional Facebook Page ID override"),
1704
1757
  }, async ({ portfolio_id, meta_access_token, meta_ad_account_id, meta_page_id }) => {
1705
- try {
1706
- const body = {};
1707
- if (meta_access_token)
1708
- body.meta_access_token = meta_access_token;
1709
- if (meta_ad_account_id)
1710
- body.meta_ad_account_id = meta_ad_account_id;
1711
- if (meta_page_id)
1712
- body.meta_page_id = meta_page_id;
1713
- const result = await client.post(`/portfolios/${portfolio_id}/launch`, body);
1714
- return formatResult(appendHint(result, "Portfolio launched. Call zuckerbot_portfolio_performance to monitor tier delivery. Use zuckerbot_rebalance_portfolio weekly to redistribute budgets based on CPA performance."));
1715
- }
1716
- catch (err) {
1717
- return formatError(err);
1718
- }
1758
+ void meta_access_token;
1759
+ void meta_ad_account_id;
1760
+ void meta_page_id;
1761
+ return formatResult({
1762
+ error: true,
1763
+ code: "portfolio_launch_temporarily_disabled",
1764
+ message: "Portfolio launch is temporarily disabled. Create a campaign with mode='legacy', review it, then call zuckerbot_launch_campaign.",
1765
+ portfolio_id,
1766
+ supported_create_mode: "legacy",
1767
+ supported_launch_tool: "zuckerbot_launch_campaign",
1768
+ });
1719
1769
  });
1720
1770
  // ── 28. Tag Creative ──────────────────────────────────────────
1721
1771
  server.tool("zuckerbot_tag_creative", "Tag Meta ads with creative attributes (hook type, visual style, product focus, CTA type, copy tone, setting) by providing ad metadata and optional asset URLs. ZuckerBot uses Claude vision to analyze the creative and store structured tags. These tags feed the zuckerbot_creative_analysis pipeline. Run this after launching new ads to keep the creative intelligence database current.", {
@@ -1759,8 +1809,10 @@ export function registerTools(server, client) {
1759
1809
  last_name: z.string().optional().describe("Optional last name for identity matching"),
1760
1810
  value: z.number().optional().describe("Optional event value override in major currency units"),
1761
1811
  event_time: z.string().optional().describe("Optional ISO 8601 event timestamp. Defaults to now."),
1762
- fbclid: z.string().optional().describe("Optional Facebook click ID for fbc cookie attribution"),
1763
- }, async ({ business_id, source_stage, crm_source, lead_id, meta_lead_id, email, phone, first_name, last_name, value, event_time, fbclid }) => {
1812
+ fbclid: z.string().optional().describe("Optional Facebook click ID; the server builds a well-formed fbc cookie from it"),
1813
+ fbc: z.string().optional().describe("Optional pre-formatted Facebook click cookie (fb.1.<ms>.<fbclid>), forwarded raw never hashed"),
1814
+ fbp: z.string().optional().describe("Optional Facebook browser ID cookie (_fbp), forwarded raw — never hashed. Improves match quality for every event"),
1815
+ }, async ({ business_id, source_stage, crm_source, lead_id, meta_lead_id, email, phone, first_name, last_name, value, event_time, fbclid, fbc, fbp }) => {
1764
1816
  try {
1765
1817
  const resolvedBusinessId = await client.resolveBusinessId(business_id);
1766
1818
  const body = {
@@ -1787,6 +1839,10 @@ export function registerTools(server, client) {
1787
1839
  body.event_time = event_time;
1788
1840
  if (fbclid)
1789
1841
  body.fbclid = fbclid;
1842
+ if (fbc)
1843
+ body.fbc = fbc;
1844
+ if (fbp)
1845
+ body.fbp = fbp;
1790
1846
  const result = await client.post("/capi/events", body);
1791
1847
  return formatResult(appendHint(result, "Event dispatched. Check the status field: 'sent' = successfully delivered to Meta, 'skipped' = CAPI disabled or stage unmapped, 'failed' = Meta rejected the event. Use zuckerbot_capi_status for aggregate delivery metrics."));
1792
1848
  }
@@ -1794,5 +1850,59 @@ export function registerTools(server, client) {
1794
1850
  return formatError(err);
1795
1851
  }
1796
1852
  });
1853
+ // ── 26. Spec Mode — Campaign From Spec ─────────────────────────
1854
+ server.tool("zuckerbot_create_campaign_from_spec", "Build a complete Meta campaign VERBATIM from a declarative JSON spec — no strategy generation, no copy authoring. Everything is created PAUSED, always; launching remains a separate deliberate call. Recommended flow: send with dry_run=true first to get the fully resolved Graph API payloads without creating anything, review them, then re-send without dry_run to build. Validation failures return per-field errors (path + message). Spec shape: campaign {name, objective OUTCOME_LEADS|OUTCOME_SALES, budget {type CBO_DAILY, amount, bid_strategy HIGHEST_VOLUME|LOWEST_COST_WITHOUT_CAP|COST_CAP}, special_ad_categories}, ad_sets [{name, conversion_location WEBSITE, pixel_id, optimisation_event {type CUSTOM_CONVERSION, id}|{type STANDARD, event}, performance_goal, attribution {click_days, view_days}, targeting {geo, age_min, advantage_audience, excluded_custom_audiences}, placements {mode MANUAL, exclude}}], ads [{name, asset {type IMAGE_SET, refs {1x1,4x5,9x16 — https URLs or uploaded image hashes}}|{type VIDEO, ref — pre-uploaded Meta video id}, primary_text, headline, description, cta, final_url, ad_set_name?}]. Use zuckerbot_list_custom_conversions to find custom conversion ids.", {
1855
+ business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
1856
+ spec: z.record(z.string(), z.any()).describe("The declarative campaign spec (see tool description for the shape)"),
1857
+ dry_run: z.boolean().optional().describe("true = return the resolved Graph payloads without creating anything. Strongly recommended before a real build"),
1858
+ }, async ({ business_id, spec, dry_run }) => {
1859
+ try {
1860
+ const resolvedBusinessId = await client.resolveBusinessId(business_id);
1861
+ const body = { business_id: resolvedBusinessId, spec };
1862
+ if (dry_run !== undefined)
1863
+ body.dry_run = dry_run;
1864
+ const result = await client.post("/campaigns/from-spec", body);
1865
+ return formatResult(appendHint(result, "If this was a dry run, review would_create then re-send without dry_run. If it built: the campaign is PAUSED and spends nothing — inspect it in Ads Manager, then launch deliberately when ready."));
1866
+ }
1867
+ catch (err) {
1868
+ return formatError(err);
1869
+ }
1870
+ });
1871
+ // ── 27. Custom Conversions — List ──────────────────────────────
1872
+ server.tool("zuckerbot_list_custom_conversions", "List the custom conversions on the connected ad account: id, name, rule, source event and pixel. Use this to find the custom conversion id a campaign spec's optimisation_event should reference.", {
1873
+ business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
1874
+ }, async ({ business_id }) => {
1875
+ try {
1876
+ const resolvedBusinessId = await client.resolveBusinessId(business_id);
1877
+ const params = new URLSearchParams({ business_id: resolvedBusinessId });
1878
+ const result = await client.get(`/custom-conversions?${params.toString()}`);
1879
+ return formatResult(appendHint(result, "Reference a conversion by id in a spec's optimisation_event: { type: 'CUSTOM_CONVERSION', id }. To create one, call zuckerbot_create_custom_conversion."));
1880
+ }
1881
+ catch (err) {
1882
+ return formatError(err);
1883
+ }
1884
+ });
1885
+ // ── 28. Custom Conversions — Create ────────────────────────────
1886
+ server.tool("zuckerbot_create_custom_conversion", "Create a custom conversion on the connected ad account (requires sufficient Graph permissions on the Meta token — returns insufficient_permission if Meta refuses). Provide the pixel, a name, optionally a rule (e.g. URL contains ...) and the source event type.", {
1887
+ business_id: z.string().optional().describe("Optional business ID override for the authenticated API key"),
1888
+ name: z.string().describe("Display name for the custom conversion"),
1889
+ pixel_id: z.string().describe("Pixel (event source) the conversion is based on"),
1890
+ custom_event_type: z.string().optional().describe("Source event type, defaults to OTHER"),
1891
+ rule: z.record(z.string(), z.any()).optional().describe("Optional Meta rule object, e.g. {\"url\":{\"i_contains\":\"/thank-you\"}}"),
1892
+ }, async ({ business_id, name, pixel_id, custom_event_type, rule }) => {
1893
+ try {
1894
+ const resolvedBusinessId = await client.resolveBusinessId(business_id);
1895
+ const body = { business_id: resolvedBusinessId, name, pixel_id };
1896
+ if (custom_event_type)
1897
+ body.custom_event_type = custom_event_type;
1898
+ if (rule)
1899
+ body.rule = rule;
1900
+ const result = await client.post("/custom-conversions", body);
1901
+ return formatResult(appendHint(result, "Custom conversion created. Reference it in a campaign spec as optimisation_event { type: 'CUSTOM_CONVERSION', id }."));
1902
+ }
1903
+ catch (err) {
1904
+ return formatError(err);
1905
+ }
1906
+ });
1797
1907
  }
1798
1908
  //# sourceMappingURL=tools.js.map