zuckerbot-mcp 0.4.0 → 0.4.2

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
  },
@@ -269,12 +269,12 @@ const businessContextTypeSchema = z
269
269
  // always available.
270
270
  function registerCreativeGenerationTools(server, client) {
271
271
  // ── Generate Static Ad ──────────────────────────────────────────
272
- server.tool("zuckerbot_generate_static_ad", "Generate a brand-aware 1080×1080 static ad image using the Ad Compositor. Automatically injects brand context (logo, colors, brand name, value props, social proof) from the business profile when business_id is provided — no need to pass brand data manually. Templates: stat_hook (statistic + supporting copy), pain_headline (bold headline + benefit bullets), testimonial (customer quote + portrait), feature_showcase (product + benefits). Returns a public image URL and QA scores. Present the image to the customer for approval; to generate more variants, call again with different parameters.", {
272
+ server.tool("zuckerbot_generate_static_ad", "Generate a brand-aware 1080×1080 static ad image. Automatically injects brand context (logo, colors, brand name, value props, social proof) from the business profile when business_id is provided — no need to pass brand data manually. Templates: stat_hook (statistic + supporting copy), pain_headline (bold headline + benefit bullets), testimonial (customer quote + portrait), feature_showcase (product + benefits). Returns a public image URL and QA scores. Present the image to the customer for approval; to generate more variants, call again with different parameters.", {
273
273
  business_id: z.string().optional().describe("Business ID for automatic brand context injection (logo, colors, CTA, testimonials)"),
274
274
  session_id: z.string().optional().describe("Architect session ID — when provided, the generated asset is linked to this campaign session"),
275
275
  template: z
276
276
  .enum(["stat_hook", "pain_headline", "testimonial", "feature_showcase"])
277
- .describe("Compositor template"),
277
+ .describe("Ad template"),
278
278
  headline: z.string().optional().describe("Primary headline (required for stat_hook, pain_headline, feature_showcase)"),
279
279
  subtext: z.string().optional().describe("Subtext below headline (stat_hook only)"),
280
280
  body: z.string().optional().describe("Body copy (stat_hook only)"),
@@ -301,8 +301,8 @@ function registerCreativeGenerationTools(server, client) {
301
301
  }
302
302
  });
303
303
  // ── Generate Video Ad ───────────────────────────────────────────
304
- server.tool("zuckerbot_generate_video_ad", "Generate a video ad by dispatching to the Ad Factory pipeline (Claude scripting → Sora/FAL video → Shotstack overlay → FFmpeg captions → Cloudinary upload). ASYNC operation: returns a job_id immediately. Poll with zuckerbot_get_video_ad_status every 30 seconds for completion. Brand context is loaded server-side. Typical generation time: 5-15 minutes. Note: Sora-generated video ads may face Meta ad review scrutiny — rejection tracking is built in.", {
305
- business_id: z.string().describe("Business ID (required for Ad Factory routing)"),
304
+ server.tool("zuckerbot_generate_video_ad", "Generate a video ad through ZuckerBot's video generation pipeline (AI scripting → AI video → captioned overlay → hosted upload). ASYNC operation: returns a job_id immediately. Poll with zuckerbot_get_video_ad_status every 30 seconds for completion. Brand context is loaded server-side. Typical generation time: 5-15 minutes. Note: AI-generated video ads may face Meta ad review scrutiny — rejection tracking is built in.", {
305
+ business_id: z.string().describe("Business ID (required for creative routing)"),
306
306
  focus: z.string().describe("Product/feature focus for the ad (e.g., 'missed_calls', 'quoting', 'scheduling')"),
307
307
  market: z.string().default("AU").describe("Target market code (AU, US, UK, NZ)"),
308
308
  ad_account_id: z.string().optional().describe("Meta ad account ID (auto-resolved if omitted)"),
@@ -357,8 +357,8 @@ function registerCreativeGenerationTools(server, client) {
357
357
  return formatError(err);
358
358
  }
359
359
  });
360
- // ── Request Creative (Ad Factory dispatch) ──────────────────────
361
- server.tool("zuckerbot_request_creative", "Dispatch a creative production request for an approved intelligence campaign. If an Ad Factory webhook is configured on the business, ZuckerBot generates full video scripts via Claude and fires the Ad Factory payload. Use this when you want ZuckerBot to handle creative production end-to-end rather than uploading your own assets.", {
360
+ // ── Request Creative (creative-pipeline dispatch) ───────────────
361
+ server.tool("zuckerbot_request_creative", "Dispatch a creative production request for an approved intelligence campaign. If a creative-production webhook is configured on the business, ZuckerBot generates full video scripts and fires the production payload. Use this when you want ZuckerBot to handle creative production end-to-end rather than uploading your own assets.", {
362
362
  campaign_id: z.string().describe("Intelligence campaign ID"),
363
363
  creative_handoff: creativeHandoffSchema.describe("Optional creative handoff configuration"),
364
364
  }, async ({ campaign_id, creative_handoff }) => {
@@ -367,14 +367,14 @@ 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);
374
374
  }
375
375
  });
376
376
  // ── Generate Creatives ──────────────────────────────────────────
377
- server.tool("zuckerbot_generate_creatives", "Generate standalone ad creative variants (images or video) using AI image models (Seedream/Imagen) or video generation (Kling). Works independently of campaign creation — useful for quick mockups, creative testing, or building assets before attaching them to a campaign. If the description mentions video, reels, UGC, or TikTok, the tool auto-routes to the Kling video model.", {
377
+ server.tool("zuckerbot_generate_creatives", "Generate standalone ad creative variants (images or video) using AI image and video models. Works independently of campaign creation — useful for quick mockups, creative testing, or building assets before attaching them to a campaign. If the description mentions video, reels, UGC, or TikTok, the tool auto-routes to the video model.", {
378
378
  business_id: z.string().optional()
379
379
  .describe("Optional business ID. When provided, brand context (name, tagline, value props, audience) is automatically injected into the generation request for more on-brand results."),
380
380
  business_name: z.string().describe("Business name"),
@@ -389,7 +389,7 @@ function registerCreativeGenerationTools(server, client) {
389
389
  model: z
390
390
  .enum(["auto", "seedream", "imagen", "kling"])
391
391
  .optional()
392
- .describe("Model selection. auto/seedream/imagen are image paths, kling is video path. Optional; inferred when omitted."),
392
+ .describe("Model selection. auto/seedream/imagen are image paths, kling is the video path. Optional; inferred from the description when omitted."),
393
393
  media_type: z
394
394
  .enum(["image", "video"])
395
395
  .optional()
@@ -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) {
@@ -583,7 +610,7 @@ export function registerTools(server, client) {
583
610
  }
584
611
  });
585
612
  // ── 3. Generate Campaign Brief ─────────────────────────────────
586
- server.tool("zuckerbot_generate_campaign_brief", "Generate a detailed creative brief from an approved campaign structure. SAFE — pure function, no Meta API calls, no money spent. Automatically pulls brand context and historical creative patterns (or uses brand context alone for cold-start accounts). Returns per-slot creative directions: for static ads, specific compositor template + headline/body/CTA + hero image prompt; for video ads, hook concept + voiceover direction + visual style. After brief is generated, present it to the customer, then call zuckerbot_generate_static_ad / zuckerbot_generate_video_ad for each slot.", {
613
+ server.tool("zuckerbot_generate_campaign_brief", "Generate a detailed creative brief from an approved campaign structure. SAFE — pure function, no Meta API calls, no money spent. Automatically pulls brand context and historical creative patterns (or uses brand context alone for cold-start accounts). Returns per-slot creative directions: for static ads, specific ad template + headline/body/CTA + hero image prompt; for video ads, hook concept + voiceover direction + visual style. After brief is generated, present it to the customer, then call zuckerbot_generate_static_ad / zuckerbot_generate_video_ad for each slot.", {
587
614
  business_id: z.string().optional().describe("Business ID (auto-resolved from API key if omitted)"),
588
615
  session_id: z.string().optional().describe("Architect session ID from zuckerbot_recommend_campaign_structure"),
589
616
  approved_structure: z.string().optional().describe("JSON string of approved campaign structure (if no session_id)"),
@@ -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. Videos from ad_factory_jobs 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. Videos from ad_factory_jobs 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);
@@ -807,13 +833,13 @@ export function registerTools(server, client) {
807
833
  if (angle_names?.length)
808
834
  body.angle_names = angle_names;
809
835
  const result = await client.post(`/campaigns/${campaign_id}/approve-strategy`, body);
810
- return formatResult(appendHint(result, "Strategy approved. Call zuckerbot_request_creative to dispatch Ad Factory, or zuckerbot_upload_creative if you already have assets ready. Alternatively, use the Campaign Architect flow: analyse_account_history → recommend_campaign_structure → generate_campaign_brief."));
836
+ return formatResult(appendHint(result, "Strategy approved. Upload your creative assets with zuckerbot_upload_creative, or use the Campaign Architect flow: analyse_account_history → recommend_campaign_structure → generate_campaign_brief."));
811
837
  }
812
838
  catch (err) {
813
839
  return formatError(err);
814
840
  }
815
841
  });
816
- server.tool("zuckerbot_upload_creative", "Upload finished creative assets (images or videos) to an approved intelligence campaign. ZuckerBot queues the Meta upload and ad-creation jobs asynchronously, then polls until they complete or the polling window expires. Use this when you have your own creative assets ready rather than using Ad Factory.", {
842
+ server.tool("zuckerbot_upload_creative", "Upload finished creative assets (images or videos) to an approved intelligence campaign. ZuckerBot queues the Meta upload and ad-creation jobs asynchronously, then polls until they complete or the polling window expires. Use this when you have your own creative assets ready.", {
817
843
  campaign_id: z.string().describe("Intelligence campaign ID"),
818
844
  creatives: z.array(creativeUploadSchema).min(1).describe("Creative assets to attach to the campaign"),
819
845
  meta_access_token: z.string().optional().describe("Optional Meta/Facebook access token override"),
@@ -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);
@@ -1036,7 +1058,7 @@ export function registerTools(server, client) {
1036
1058
  if (summary_mode)
1037
1059
  params.set("summary_mode", "true");
1038
1060
  const result = await client.get(`/creative/analysis?${params.toString()}`);
1039
- return formatResult(appendHint(result, "Analysis complete. Call zuckerbot_creative_cross_analysis to find winning attribute combinations, rerun with include_ads=true to inspect the best and worst individual ads, or use zuckerbot_generate_briefs to produce new Ad Factory briefs weighted toward the strongest patterns. Feed these insights into zuckerbot_recommend_campaign_structure for data-driven campaign planning."));
1061
+ return formatResult(appendHint(result, "Analysis complete. Call zuckerbot_creative_cross_analysis to find winning attribute combinations, rerun with include_ads=true to inspect the best and worst individual ads, or use zuckerbot_generate_briefs to produce new creative briefs weighted toward the strongest patterns. Feed these insights into zuckerbot_recommend_campaign_structure for data-driven campaign planning."));
1040
1062
  }
1041
1063
  catch (err) {
1042
1064
  return formatError(err);
@@ -1081,13 +1103,13 @@ export function registerTools(server, client) {
1081
1103
  return formatError(err);
1082
1104
  }
1083
1105
  });
1084
- server.tool("zuckerbot_generate_briefs", "Generate Ad Factory-compatible creative production briefs based on the business's tagged ad-performance patterns. Each brief specifies hook type, visual style, copy tone, CTA, and script guidance weighted toward the top-performing creative attributes. Use after running zuckerbot_creative_analysis to know which patterns to bias toward.", {
1106
+ server.tool("zuckerbot_generate_briefs", "Generate creative production briefs based on the business's tagged ad-performance patterns. Each brief specifies hook type, visual style, copy tone, CTA, and script guidance weighted toward the top-performing creative attributes. Use after running zuckerbot_creative_analysis to know which patterns to bias toward.", {
1085
1107
  business_id: z.string().optional().describe("Optional business ID override"),
1086
1108
  count: z.number().int().min(1).max(10).optional().describe("How many briefs to generate. Defaults to 5."),
1087
1109
  bias: z.string().optional().describe("Optional generation bias, for example performance or exploration"),
1088
1110
  exclude_angles: z.array(z.string()).optional().describe("Optional angles or product focuses to avoid"),
1089
1111
  target_market: z.string().optional().describe("Optional target market override, for example AU or US"),
1090
- font_preset: z.string().optional().describe("Optional Ad Factory font preset override"),
1112
+ font_preset: z.string().optional().describe("Optional font preset override"),
1091
1113
  metric: z.enum(["cpl", "ctr", "cpc", "frequency"]).optional().describe("Optional ranking metric. Defaults to cpl."),
1092
1114
  }, async ({ business_id, count, bias, exclude_angles, target_market, font_preset, metric }) => {
1093
1115
  try {
@@ -1106,7 +1128,7 @@ export function registerTools(server, client) {
1106
1128
  if (metric)
1107
1129
  body.metric = metric;
1108
1130
  const result = await client.post("/creative/generate-briefs", body);
1109
- return formatResult(appendHint(result, "Briefs generated. Pass them to your creative team or Ad Factory for production. Once assets are ready, use zuckerbot_upload_creative to attach them to a campaign, or zuckerbot_generate_creatives to generate AI images/video from these concepts. For automated brief generation tied to a campaign plan, use zuckerbot_generate_campaign_brief."));
1131
+ return formatResult(appendHint(result, "Briefs generated. Pass them to your creative team for production. Once assets are ready, use zuckerbot_upload_creative to attach them to a campaign. For automated brief generation tied to a campaign plan, use zuckerbot_generate_campaign_brief."));
1110
1132
  }
1111
1133
  catch (err) {
1112
1134
  return formatError(err);
@@ -1580,7 +1602,7 @@ export function registerTools(server, client) {
1580
1602
  }
1581
1603
  });
1582
1604
  // ── 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.", {
1605
+ 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
1606
  business_id: z.string().optional().describe("Optional business ID override"),
1585
1607
  template_id: z.string().optional().describe("Optional portfolio template ID"),
1586
1608
  template_name: z.string().optional().describe("Optional portfolio template name, such as 'Local Services'"),
@@ -1615,19 +1637,19 @@ export function registerTools(server, client) {
1615
1637
  if (tiers)
1616
1638
  body.tiers = tiers;
1617
1639
  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."));
1640
+ 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
1641
  }
1620
1642
  catch (err) {
1621
1643
  return formatError(err);
1622
1644
  }
1623
1645
  });
1624
1646
  // ── 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.", {
1647
+ 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
1648
  portfolio_id: z.string().describe("Audience portfolio ID"),
1627
1649
  }, async ({ portfolio_id }) => {
1628
1650
  try {
1629
1651
  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."));
1652
+ 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
1653
  }
1632
1654
  catch (err) {
1633
1655
  return formatError(err);
@@ -1696,26 +1718,23 @@ export function registerTools(server, client) {
1696
1718
  }
1697
1719
  });
1698
1720
  // ── 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.", {
1721
+ 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
1722
  portfolio_id: z.string().describe("Audience portfolio ID to launch"),
1701
1723
  meta_access_token: z.string().optional().describe("Optional Meta/Facebook access token override"),
1702
1724
  meta_ad_account_id: z.string().optional().describe("Optional Meta ad account ID override (format: act_XXXXX)"),
1703
1725
  meta_page_id: z.string().optional().describe("Optional Facebook Page ID override"),
1704
1726
  }, 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
- }
1727
+ void meta_access_token;
1728
+ void meta_ad_account_id;
1729
+ void meta_page_id;
1730
+ return formatResult({
1731
+ error: true,
1732
+ code: "portfolio_launch_temporarily_disabled",
1733
+ message: "Portfolio launch is temporarily disabled. Create a campaign with mode='legacy', review it, then call zuckerbot_launch_campaign.",
1734
+ portfolio_id,
1735
+ supported_create_mode: "legacy",
1736
+ supported_launch_tool: "zuckerbot_launch_campaign",
1737
+ });
1719
1738
  });
1720
1739
  // ── 28. Tag Creative ──────────────────────────────────────────
1721
1740
  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.", {