@productmaker/mcp 0.11.0 → 0.12.1

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.
Files changed (3) hide show
  1. package/dist/http.js +74 -65
  2. package/dist/stdio.js +74 -65
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -702,71 +702,71 @@ var McpErrorCode = {
702
702
  };
703
703
  var CODE_MESSAGES = {
704
704
  // Shopify
705
- SHOPIFY_NOT_CONNECTED: "No tienes ninguna tienda Shopify conectada. Conecta una en https://productmaker.app/integrations antes de publicar.",
706
- SHOPIFY_TOKEN_EXPIRED: "La sesi\xF3n con Shopify expir\xF3. Reconecta la tienda en https://productmaker.app/integrations.",
707
- SHOPIFY_SCOPE_UPGRADE_REQUIRED: "La conexi\xF3n con Shopify necesita permisos adicionales. Recon\xE9ctala en https://productmaker.app/integrations para aprobar los nuevos permisos.",
708
- SHOPIFY_PUBLISH_FAILED: "Shopify rechaz\xF3 la publicaci\xF3n. Verifica que el producto/landing no exista ya en la tienda y que tu plan permita publicar m\xE1s productos.",
705
+ SHOPIFY_NOT_CONNECTED: "No Shopify store is connected. Connect one at https://productmaker.app/integrations before publishing.",
706
+ SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
707
+ SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
708
+ SHOPIFY_PUBLISH_FAILED: "Shopify rejected the publish. Check that the product/landing does not already exist on the store and that your Shopify plan allows additional products.",
709
709
  // Meta — high-frequency
710
- META_NO_FB_PAGE: "No encontramos una P\xE1gina de Facebook asociada a tu cuenta. Crea una en https://www.facebook.com/pages/create antes de publicar (Meta exige una Page para correr ads).",
711
- META_NO_ADS_CREATED: "No se pudieron crear los ads. Lo m\xE1s probable: la cuenta de Meta Ads no tiene m\xE9todo de pago, falta vincular una P\xE1gina, o la app de Facebook perdi\xF3 permisos. Verifica los tres en https://business.facebook.com.",
712
- META_NO_ASSETS_READY: "La tarea a\xFAn no tiene videos ni ad creative listos para publicar. Espera a que termine la generaci\xF3n o regenera los assets.",
713
- META_NO_THUMBNAIL_SOURCE: "No hay imagen disponible para usar de portada del video. Sube una imagen de producto o genera el ad creative antes de publicar.",
714
- META_NO_LANDING_URL: "Falta la URL de la landing. P\xE1sala en `landingUrl` o publica primero a Shopify con `publish_to_shopify`.",
715
- META_TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
716
- META_INVALID_DATA: "Meta rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica que `adAccountId` y `pixelId` sean correctos.",
717
- META_INVALID_AD_ACCOUNT: "La cuenta publicitaria no existe o no tienes acceso. Confirma el `adAccountId` con `list_meta_ad_accounts`.",
718
- META_MISSING_PAYMENT: "Tu cuenta de Meta Ads no tiene m\xE9todo de pago configurado. Visita https://business.facebook.com/billing_hub/payment_settings, selecciona la cuenta publicitaria y agrega una tarjeta antes de reintentar.",
719
- META_TOKEN_EXPIRED: "Tu sesi\xF3n con Meta expir\xF3. Reconecta Facebook en https://productmaker.app/integrations.",
720
- META_ACCOUNT_RESTRICTED: "Tu cuenta de Meta Ads est\xE1 temporalmente restringida para crear anuncios. Revisa avisos en https://business.facebook.com y completa la verificaci\xF3n requerida.",
721
- META_CAPABILITY_MISSING: "Tu app de Facebook no tiene los permisos requeridos para esta cuenta publicitaria. Reconecta Facebook en https://productmaker.app/integrations y acepta TODOS los permisos solicitados (incluyendo business_management).",
722
- META_PERMISSION_DENIED: "Tu conexi\xF3n de Facebook no tiene los permisos suficientes. Recon\xE9ctala en https://productmaker.app/integrations y acepta todos los permisos.",
723
- META_PIXEL_NOT_FOUND: "El Pixel no existe o no est\xE1 asociado a la cuenta publicitaria. Verifica el `pixelId` en https://business.facebook.com/events_manager.",
724
- META_OBJECT_NOT_FOUND: "El objeto solicitado (campa\xF1a/ad set/ad) no existe en Meta. Verifica el ID o crea uno nuevo.",
725
- META_AI_DISCLOSURE_REQUIRED: "Meta rechaz\xF3 el ad por contenido IA no declarado. El sistema agrega la divulgaci\xF3n autom\xE1ticamente; si persiste, edita la copy para evitar lenguaje que Meta marque como enga\xF1oso.",
726
- META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requiere un monto de puja expl\xEDcito para esta cuenta/presupuesto. Sube el presupuesto diario o desactiva las pujas con tope a nivel cuenta.",
727
- META_CBO_BUDGET_CONFLICT: "Conflicto de presupuesto entre la campa\xF1a (CBO) y el ad set. No se puede mezclar.",
728
- META_BUDGET_MODE_CONFLICT: "Meta recibi\xF3 presupuesto a nivel campa\xF1a Y a nivel ad set. Define s\xF3lo uno.",
729
- META_AD_SET_BUDGET_REQUIRED: "Esta configuraci\xF3n de campa\xF1a requiere presupuesto a nivel ad set, no a nivel campa\xF1a.",
730
- META_DAILY_BUDGET_TOO_HIGH: "El presupuesto diario excede el m\xE1ximo permitido por Meta para tu cuenta. B\xE1jalo y reintenta.",
731
- META_CAMPAIGN_CONFIG_INVALID: "La configuraci\xF3n de la campa\xF1a es inv\xE1lida. Verifica `dailyBudgetUsd`, `targetCountries` y `campaignName`.",
732
- META_WHATSAPP_DESTINATION_DISABLED: "El destino WhatsApp no est\xE1 habilitado para esta cuenta de Meta. Cambia el destino del ad o habilita WhatsApp Business en la cuenta.",
710
+ META_NO_FB_PAGE: "No Facebook Page is associated with this account. Create one at https://www.facebook.com/pages/create before publishing (Meta requires a Page to run ads).",
711
+ META_NO_ADS_CREATED: "No ads could be created. Most likely: the Meta Ads account has no payment method, no Facebook Page is linked, or the Facebook app lost permissions. Verify all three at https://business.facebook.com.",
712
+ META_NO_ASSETS_READY: "The task has no videos or ad creative ready to publish yet. Wait for generation to finish or regenerate the assets.",
713
+ META_NO_THUMBNAIL_SOURCE: "No image is available to use as the video thumbnail. Upload a product image or generate the ad creative before publishing.",
714
+ META_NO_LANDING_URL: "Missing landing URL. Pass it in `landingUrl` or publish to Shopify first with `publish_to_shopify`.",
715
+ META_TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
716
+ META_INVALID_DATA: "Meta rejected the request due to invalid data. Verify that `adAccountId` and `pixelId` are correct.",
717
+ META_INVALID_AD_ACCOUNT: "Ad account not found or no access. Confirm the `adAccountId` with `list_meta_ad_accounts`.",
718
+ META_MISSING_PAYMENT: "Your Meta Ads account has no payment method configured. Visit https://business.facebook.com/billing_hub/payment_settings, select the ad account, and add a card before retrying.",
719
+ META_TOKEN_EXPIRED: "Your Meta session expired. Reconnect Facebook at https://productmaker.app/integrations.",
720
+ META_ACCOUNT_RESTRICTED: "Your Meta Ads account is temporarily restricted from creating ads. Check the notices at https://business.facebook.com and complete the required verification.",
721
+ META_CAPABILITY_MISSING: "The Facebook app lacks required permissions for this ad account. Reconnect Facebook at https://productmaker.app/integrations and accept ALL requested permissions (including business_management).",
722
+ META_PERMISSION_DENIED: "The Facebook connection lacks sufficient permissions. Reconnect it at https://productmaker.app/integrations and accept all permissions.",
723
+ META_PIXEL_NOT_FOUND: "Pixel not found or not associated with the ad account. Verify the `pixelId` at https://business.facebook.com/events_manager.",
724
+ META_OBJECT_NOT_FOUND: "The requested object (campaign/ad set/ad) does not exist in Meta. Verify the ID or create a new one.",
725
+ META_AI_DISCLOSURE_REQUIRED: "Meta rejected the ad for undisclosed AI content. The system adds the disclosure automatically; if it persists, edit the copy to avoid language Meta flags as misleading.",
726
+ META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requires an explicit bid amount for this account/budget. Raise the daily budget or disable account-level bid caps.",
727
+ META_CBO_BUDGET_CONFLICT: "Budget conflict between campaign (CBO) and ad set. They cannot be mixed.",
728
+ META_BUDGET_MODE_CONFLICT: "Meta received budget at BOTH campaign and ad set level. Set only one.",
729
+ META_AD_SET_BUDGET_REQUIRED: "This campaign configuration requires ad-set-level budget, not campaign-level.",
730
+ META_DAILY_BUDGET_TOO_HIGH: "The daily budget exceeds the maximum allowed by Meta for this account. Lower it and retry.",
731
+ META_CAMPAIGN_CONFIG_INVALID: "Campaign configuration is invalid. Verify `dailyBudgetUsd`, `targetCountries`, and `campaignName`.",
732
+ META_WHATSAPP_DESTINATION_DISABLED: "WhatsApp destination is not enabled for this Meta account. Change the ad destination or enable WhatsApp Business on the account.",
733
733
  // TikTok
734
- TIKTOK_NOT_CONNECTED: "No tienes TikTok Ads conectado. Con\xE9ctalo en https://productmaker.app/integrations.",
735
- TIKTOK_TOKEN_EXPIRED: "La sesi\xF3n con TikTok expir\xF3. Recon\xE9ctalo en https://productmaker.app/integrations.",
736
- TIKTOK_PERMISSION_DENIED: "Tu conexi\xF3n de TikTok no tiene los permisos suficientes. Recon\xE9ctala y acepta todos los permisos solicitados.",
737
- TIKTOK_MISSING_PAYMENT: "Tu cuenta de TikTok Ads no tiene m\xE9todo de pago configurado. Agrega uno en https://ads.tiktok.com.",
738
- TIKTOK_PIXEL_NOT_FOUND: "El Pixel de TikTok no existe o no est\xE1 asociado al advertiser. Verifica el `pixelId`.",
739
- TIKTOK_INVALID_ADVERTISER: "El advertiser no existe o no tienes acceso. Confirma el `advertiserId` con `list_tiktok_advertisers`.",
740
- TIKTOK_ADVERTISER_REQUIRED: "Falta el `advertiserId`. Obt\xE9n uno con `list_tiktok_advertisers` antes de publicar.",
741
- TIKTOK_IDENTITY_REQUIRED: "TikTok requiere una identidad (perfil del anunciante) para publicar. Crea una en https://ads.tiktok.com.",
742
- TIKTOK_IDENTITY_API_REQUIRED: "TikTok requiere habilitar la API de identidades en tu cuenta. Contacta a TikTok Ads Support.",
743
- TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rechaz\xF3 la subida del video. Verifica que el formato sea MP4 < 500MB.",
744
- TIKTOK_VIDEO_UPLOAD_TIMEOUT: "La subida del video a TikTok se demor\xF3 demasiado. Reintenta en unos minutos.",
745
- TIKTOK_RATE_LIMITED: "TikTok limit\xF3 las solicitudes. Espera unos minutos antes de reintentar.",
746
- TIKTOK_SERVER_ERROR: "TikTok devolvi\xF3 un error de servidor. Reintenta en unos minutos.",
747
- TIKTOK_INVALID_DATA: "TikTok rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica `advertiserId`, `pixelId` y `campaignConfig`.",
734
+ TIKTOK_NOT_CONNECTED: "TikTok Ads is not connected. Connect it at https://productmaker.app/integrations.",
735
+ TIKTOK_TOKEN_EXPIRED: "The TikTok session expired. Reconnect it at https://productmaker.app/integrations.",
736
+ TIKTOK_PERMISSION_DENIED: "The TikTok connection lacks sufficient permissions. Reconnect it and accept all requested permissions.",
737
+ TIKTOK_MISSING_PAYMENT: "Your TikTok Ads account has no payment method configured. Add one at https://ads.tiktok.com.",
738
+ TIKTOK_PIXEL_NOT_FOUND: "TikTok Pixel not found or not associated with the advertiser. Verify the `pixelId`.",
739
+ TIKTOK_INVALID_ADVERTISER: "Advertiser not found or no access. Confirm the `advertiserId` with `list_tiktok_advertisers`.",
740
+ TIKTOK_ADVERTISER_REQUIRED: "Missing `advertiserId`. Obtain one with `list_tiktok_advertisers` before publishing.",
741
+ TIKTOK_IDENTITY_REQUIRED: "TikTok requires an identity (advertiser profile) to publish. Create one at https://ads.tiktok.com.",
742
+ TIKTOK_IDENTITY_API_REQUIRED: "TikTok requires the identities API to be enabled on this account. Contact TikTok Ads Support.",
743
+ TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rejected the video upload. Verify the format is MP4 under 500MB.",
744
+ TIKTOK_VIDEO_UPLOAD_TIMEOUT: "The video upload to TikTok timed out. Retry in a few minutes.",
745
+ TIKTOK_RATE_LIMITED: "TikTok rate-limited the request. Wait a few minutes before retrying.",
746
+ TIKTOK_SERVER_ERROR: "TikTok returned a server error. Retry in a few minutes.",
747
+ TIKTOK_INVALID_DATA: "TikTok rejected the request due to invalid data. Verify `advertiserId`, `pixelId`, and `campaignConfig`.",
748
748
  // Campaign (shared)
749
- CAMPAIGN_NOT_FOUND: "La campa\xF1a no existe o no pertenece a este usuario. Verifica el `campaignId`.",
750
- CAMPAIGN_TASK_NOT_FOUND: "La tarea de la campa\xF1a no existe. Verifica el `taskId`.",
751
- CAMPAIGN_MISSING_LANDING_URL: "Falta la URL de la landing en la campa\xF1a. Publica primero a Shopify o pasa `landingUrl` expl\xEDcitamente.",
752
- CAMPAIGN_NO_CREATIVES: "La tarea no tiene creativos (video/imagen) listos para la campa\xF1a. Espera a que termine la generaci\xF3n.",
753
- CAMPAIGN_MISSING_THUMBNAILS: "Faltan miniaturas para los videos. Verifica que la generaci\xF3n de imagen/video haya terminado.",
749
+ CAMPAIGN_NOT_FOUND: "Campaign not found or does not belong to this user. Verify the `campaignId`.",
750
+ CAMPAIGN_TASK_NOT_FOUND: "Campaign task not found. Verify the `taskId`.",
751
+ CAMPAIGN_MISSING_LANDING_URL: "Missing landing URL for the campaign. Publish to Shopify first or pass `landingUrl` explicitly.",
752
+ CAMPAIGN_NO_CREATIVES: "The task has no creatives (video/image) ready for the campaign. Wait for generation to finish.",
753
+ CAMPAIGN_MISSING_THUMBNAILS: "Missing thumbnails for the videos. Verify that image/video generation has finished.",
754
754
  // Connections
755
- NO_SHOPIFY_SHOPS: "No hay tiendas Shopify conectadas. Conecta una en https://productmaker.app/integrations.",
756
- NO_META_AD_ACCOUNTS: "No hay cuentas de Meta Ads conectadas. Conecta Facebook en https://productmaker.app/integrations.",
755
+ NO_SHOPIFY_SHOPS: "No Shopify stores connected. Connect one at https://productmaker.app/integrations.",
756
+ NO_META_AD_ACCOUNTS: "No Meta Ads accounts connected. Connect Facebook at https://productmaker.app/integrations.",
757
757
  // Task lifecycle
758
- TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
759
- TASK_NOT_READY: "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n. Espera a que termine y reintenta.",
758
+ TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
759
+ TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
760
760
  // API key
761
- INVALID_API_KEY: "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.",
762
- REVOKED_API_KEY: "API key revocada. Genera una nueva en https://productmaker.app/account/api-keys.",
761
+ INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
762
+ REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
763
763
  // Rate
764
- RATE_LIMITED: "Demasiadas solicitudes. Espera unos segundos.",
764
+ RATE_LIMITED: "Too many requests. Wait a few seconds.",
765
765
  // Generic
766
- UPSTREAM_UNAVAILABLE: "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.",
767
- UPSTREAM_TIMEOUT: "Tiempo de espera agotado. Reintenta en un momento.",
768
- INTERNAL: "Error interno.",
769
- INVALID_INPUT: "Entrada inv\xE1lida."
766
+ UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
767
+ UPSTREAM_TIMEOUT: "Request timed out. Retry shortly.",
768
+ INTERNAL: "Internal error.",
769
+ INVALID_INPUT: "Invalid input."
770
770
  };
771
771
  var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
772
772
  // Input/auth
@@ -906,11 +906,11 @@ function resolveMessage(code, backendMessage) {
906
906
  const mapped = CODE_MESSAGES[code];
907
907
  const backend = typeof backendMessage === "string" ? backendMessage.trim() : "";
908
908
  if (mapped && backend && looksLikeMeaningfulBackendMessage(backend, mapped)) {
909
- return `${mapped} (Detalle: ${backend})`;
909
+ return `${mapped} (Detail: ${backend})`;
910
910
  }
911
911
  if (mapped) return mapped;
912
912
  if (backend) return backend;
913
- return `El servicio devolvi\xF3 un error (c\xF3digo: ${code}). Intenta de nuevo en un momento o contacta soporte si persiste.`;
913
+ return `The service returned an error (code: ${code}). Try again shortly or contact support if it persists.`;
914
914
  }
915
915
  __name(resolveMessage, "resolveMessage");
916
916
  function translateError(err) {
@@ -1546,7 +1546,7 @@ var RegenerateVideoScriptInput = z4.object({
1546
1546
  narrativeStyle: NarrativeStyleEnum.describe('Narrativa para el nuevo guion (mismos valores que angleNarratives). REQUERIDO \u2014 el backend NO acepta "mantener la actual"; siempre tienes que elegir una. Si el usuario quiere re-rolear con la misma narrativa, pasa la que ya tiene el \xE1ngulo (la ves en get_video_script o en get_task_status).')
1547
1547
  }).strict();
1548
1548
  function registerScriptReviewTools(server, api) {
1549
- server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId? }] }.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
1549
+ server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId? }], error? }. El campo `error` (cuando viene con status="ready") es el motivo legible por el que un intento previo de aprobaci\xF3n fue rechazado (p.ej. el guion no cab\xEDa en el tiempo y el \xE1ngulo volvi\xF3 a "ready"); mu\xE9straselo al usuario.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
1550
1550
  try {
1551
1551
  const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
1552
1552
  const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
@@ -1566,7 +1566,7 @@ function registerScriptReviewTools(server, api) {
1566
1566
  throw translateError(e);
1567
1567
  }
1568
1568
  });
1569
- server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO las valida si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas). Si quieres que pasen tal cual, simplemente no las incluyas en scenes[].\n4. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n5. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
1569
+ server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO valida el rango POR-ESCENA si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas).\n4. TOPE GLOBAL: adem\xE1s del rango por-escena, el backend rechaza con 400 ("El guion no cabe en el tiempo del video. Ac\xF3rtalo y vuelve a aprobar.") cuando el TOTAL de palabras de TODO el guion (incluidas las escenas que no editaste) supera el cupo de la voz \u2014 porque ese guion se pasar\xEDa del tope de duraci\xF3n al renderizar. Si lo ves, acorta las escenas m\xE1s largas (la suma de `maxWords` de get_video_script es el techo total) y reintenta; o usa regenerate_video_script. Este 400 es s\xEDncrono y NO consume render.\n5. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n6. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
1570
1570
  try {
1571
1571
  const body = {
1572
1572
  scenes: input.scenes
@@ -1642,7 +1642,11 @@ var MetaInput = z6.object({
1642
1642
  /** Select which angles to publish by 0-based index. Omit to publish all
1643
1643
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1644
1644
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1645
- campaignConfig: McpCampaignConfig.optional()
1645
+ campaignConfig: McpCampaignConfig.optional(),
1646
+ /** Override the ad's primary text (body copy). If omitted, ProductMaker
1647
+ * uses the auto-generated copy attached to the task. Capped at 500 chars
1648
+ * (Meta's primary-text limit). */
1649
+ primaryText: z6.string().trim().min(1).max(500).optional()
1646
1650
  });
1647
1651
  var TikTokInput = z6.object({
1648
1652
  taskId: z6.string().min(1),
@@ -1650,7 +1654,10 @@ var TikTokInput = z6.object({
1650
1654
  pixelId: z6.string().min(1).optional(),
1651
1655
  landingUrl: z6.string().url().optional(),
1652
1656
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1653
- campaignConfig: McpCampaignConfig.optional()
1657
+ campaignConfig: McpCampaignConfig.optional(),
1658
+ /** Override the ad's description (TikTok's body copy). If omitted,
1659
+ * ProductMaker uses the auto-generated copy attached to the task. */
1660
+ primaryText: z6.string().trim().min(1).max(500).optional()
1654
1661
  });
1655
1662
  function registerPublishTools(server, api) {
1656
1663
  server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto publica TODAS las im\xE1genes que la tarea gener\xF3 para la landing; pasa `numLandingImages` s\xF3lo si el usuario te pide menos. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
@@ -1675,7 +1682,7 @@ function registerPublishTools(server, api) {
1675
1682
  throw translateError(e);
1676
1683
  }
1677
1684
  });
1678
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1685
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1679
1686
  try {
1680
1687
  const body = {
1681
1688
  ...rest
@@ -1683,6 +1690,7 @@ function registerPublishTools(server, api) {
1683
1690
  if (campaignConfig) {
1684
1691
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1685
1692
  }
1693
+ if (primaryText) body.primaryText = primaryText;
1686
1694
  const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1687
1695
  return asText({
1688
1696
  ...r,
@@ -1692,7 +1700,7 @@ function registerPublishTools(server, api) {
1692
1700
  throw translateError(e);
1693
1701
  }
1694
1702
  });
1695
- server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1703
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1696
1704
  try {
1697
1705
  const body = {
1698
1706
  ...rest
@@ -1700,6 +1708,7 @@ function registerPublishTools(server, api) {
1700
1708
  if (campaignConfig) {
1701
1709
  body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1702
1710
  }
1711
+ if (primaryText) body.primaryText = primaryText;
1703
1712
  const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1704
1713
  return asText({
1705
1714
  ...r,
package/dist/stdio.js CHANGED
@@ -699,71 +699,71 @@ var McpErrorCode = {
699
699
  };
700
700
  var CODE_MESSAGES = {
701
701
  // Shopify
702
- SHOPIFY_NOT_CONNECTED: "No tienes ninguna tienda Shopify conectada. Conecta una en https://productmaker.app/integrations antes de publicar.",
703
- SHOPIFY_TOKEN_EXPIRED: "La sesi\xF3n con Shopify expir\xF3. Reconecta la tienda en https://productmaker.app/integrations.",
704
- SHOPIFY_SCOPE_UPGRADE_REQUIRED: "La conexi\xF3n con Shopify necesita permisos adicionales. Recon\xE9ctala en https://productmaker.app/integrations para aprobar los nuevos permisos.",
705
- SHOPIFY_PUBLISH_FAILED: "Shopify rechaz\xF3 la publicaci\xF3n. Verifica que el producto/landing no exista ya en la tienda y que tu plan permita publicar m\xE1s productos.",
702
+ SHOPIFY_NOT_CONNECTED: "No Shopify store is connected. Connect one at https://productmaker.app/integrations before publishing.",
703
+ SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
704
+ SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
705
+ SHOPIFY_PUBLISH_FAILED: "Shopify rejected the publish. Check that the product/landing does not already exist on the store and that your Shopify plan allows additional products.",
706
706
  // Meta — high-frequency
707
- META_NO_FB_PAGE: "No encontramos una P\xE1gina de Facebook asociada a tu cuenta. Crea una en https://www.facebook.com/pages/create antes de publicar (Meta exige una Page para correr ads).",
708
- META_NO_ADS_CREATED: "No se pudieron crear los ads. Lo m\xE1s probable: la cuenta de Meta Ads no tiene m\xE9todo de pago, falta vincular una P\xE1gina, o la app de Facebook perdi\xF3 permisos. Verifica los tres en https://business.facebook.com.",
709
- META_NO_ASSETS_READY: "La tarea a\xFAn no tiene videos ni ad creative listos para publicar. Espera a que termine la generaci\xF3n o regenera los assets.",
710
- META_NO_THUMBNAIL_SOURCE: "No hay imagen disponible para usar de portada del video. Sube una imagen de producto o genera el ad creative antes de publicar.",
711
- META_NO_LANDING_URL: "Falta la URL de la landing. P\xE1sala en `landingUrl` o publica primero a Shopify con `publish_to_shopify`.",
712
- META_TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
713
- META_INVALID_DATA: "Meta rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica que `adAccountId` y `pixelId` sean correctos.",
714
- META_INVALID_AD_ACCOUNT: "La cuenta publicitaria no existe o no tienes acceso. Confirma el `adAccountId` con `list_meta_ad_accounts`.",
715
- META_MISSING_PAYMENT: "Tu cuenta de Meta Ads no tiene m\xE9todo de pago configurado. Visita https://business.facebook.com/billing_hub/payment_settings, selecciona la cuenta publicitaria y agrega una tarjeta antes de reintentar.",
716
- META_TOKEN_EXPIRED: "Tu sesi\xF3n con Meta expir\xF3. Reconecta Facebook en https://productmaker.app/integrations.",
717
- META_ACCOUNT_RESTRICTED: "Tu cuenta de Meta Ads est\xE1 temporalmente restringida para crear anuncios. Revisa avisos en https://business.facebook.com y completa la verificaci\xF3n requerida.",
718
- META_CAPABILITY_MISSING: "Tu app de Facebook no tiene los permisos requeridos para esta cuenta publicitaria. Reconecta Facebook en https://productmaker.app/integrations y acepta TODOS los permisos solicitados (incluyendo business_management).",
719
- META_PERMISSION_DENIED: "Tu conexi\xF3n de Facebook no tiene los permisos suficientes. Recon\xE9ctala en https://productmaker.app/integrations y acepta todos los permisos.",
720
- META_PIXEL_NOT_FOUND: "El Pixel no existe o no est\xE1 asociado a la cuenta publicitaria. Verifica el `pixelId` en https://business.facebook.com/events_manager.",
721
- META_OBJECT_NOT_FOUND: "El objeto solicitado (campa\xF1a/ad set/ad) no existe en Meta. Verifica el ID o crea uno nuevo.",
722
- META_AI_DISCLOSURE_REQUIRED: "Meta rechaz\xF3 el ad por contenido IA no declarado. El sistema agrega la divulgaci\xF3n autom\xE1ticamente; si persiste, edita la copy para evitar lenguaje que Meta marque como enga\xF1oso.",
723
- META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requiere un monto de puja expl\xEDcito para esta cuenta/presupuesto. Sube el presupuesto diario o desactiva las pujas con tope a nivel cuenta.",
724
- META_CBO_BUDGET_CONFLICT: "Conflicto de presupuesto entre la campa\xF1a (CBO) y el ad set. No se puede mezclar.",
725
- META_BUDGET_MODE_CONFLICT: "Meta recibi\xF3 presupuesto a nivel campa\xF1a Y a nivel ad set. Define s\xF3lo uno.",
726
- META_AD_SET_BUDGET_REQUIRED: "Esta configuraci\xF3n de campa\xF1a requiere presupuesto a nivel ad set, no a nivel campa\xF1a.",
727
- META_DAILY_BUDGET_TOO_HIGH: "El presupuesto diario excede el m\xE1ximo permitido por Meta para tu cuenta. B\xE1jalo y reintenta.",
728
- META_CAMPAIGN_CONFIG_INVALID: "La configuraci\xF3n de la campa\xF1a es inv\xE1lida. Verifica `dailyBudgetUsd`, `targetCountries` y `campaignName`.",
729
- META_WHATSAPP_DESTINATION_DISABLED: "El destino WhatsApp no est\xE1 habilitado para esta cuenta de Meta. Cambia el destino del ad o habilita WhatsApp Business en la cuenta.",
707
+ META_NO_FB_PAGE: "No Facebook Page is associated with this account. Create one at https://www.facebook.com/pages/create before publishing (Meta requires a Page to run ads).",
708
+ META_NO_ADS_CREATED: "No ads could be created. Most likely: the Meta Ads account has no payment method, no Facebook Page is linked, or the Facebook app lost permissions. Verify all three at https://business.facebook.com.",
709
+ META_NO_ASSETS_READY: "The task has no videos or ad creative ready to publish yet. Wait for generation to finish or regenerate the assets.",
710
+ META_NO_THUMBNAIL_SOURCE: "No image is available to use as the video thumbnail. Upload a product image or generate the ad creative before publishing.",
711
+ META_NO_LANDING_URL: "Missing landing URL. Pass it in `landingUrl` or publish to Shopify first with `publish_to_shopify`.",
712
+ META_TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
713
+ META_INVALID_DATA: "Meta rejected the request due to invalid data. Verify that `adAccountId` and `pixelId` are correct.",
714
+ META_INVALID_AD_ACCOUNT: "Ad account not found or no access. Confirm the `adAccountId` with `list_meta_ad_accounts`.",
715
+ META_MISSING_PAYMENT: "Your Meta Ads account has no payment method configured. Visit https://business.facebook.com/billing_hub/payment_settings, select the ad account, and add a card before retrying.",
716
+ META_TOKEN_EXPIRED: "Your Meta session expired. Reconnect Facebook at https://productmaker.app/integrations.",
717
+ META_ACCOUNT_RESTRICTED: "Your Meta Ads account is temporarily restricted from creating ads. Check the notices at https://business.facebook.com and complete the required verification.",
718
+ META_CAPABILITY_MISSING: "The Facebook app lacks required permissions for this ad account. Reconnect Facebook at https://productmaker.app/integrations and accept ALL requested permissions (including business_management).",
719
+ META_PERMISSION_DENIED: "The Facebook connection lacks sufficient permissions. Reconnect it at https://productmaker.app/integrations and accept all permissions.",
720
+ META_PIXEL_NOT_FOUND: "Pixel not found or not associated with the ad account. Verify the `pixelId` at https://business.facebook.com/events_manager.",
721
+ META_OBJECT_NOT_FOUND: "The requested object (campaign/ad set/ad) does not exist in Meta. Verify the ID or create a new one.",
722
+ META_AI_DISCLOSURE_REQUIRED: "Meta rejected the ad for undisclosed AI content. The system adds the disclosure automatically; if it persists, edit the copy to avoid language Meta flags as misleading.",
723
+ META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requires an explicit bid amount for this account/budget. Raise the daily budget or disable account-level bid caps.",
724
+ META_CBO_BUDGET_CONFLICT: "Budget conflict between campaign (CBO) and ad set. They cannot be mixed.",
725
+ META_BUDGET_MODE_CONFLICT: "Meta received budget at BOTH campaign and ad set level. Set only one.",
726
+ META_AD_SET_BUDGET_REQUIRED: "This campaign configuration requires ad-set-level budget, not campaign-level.",
727
+ META_DAILY_BUDGET_TOO_HIGH: "The daily budget exceeds the maximum allowed by Meta for this account. Lower it and retry.",
728
+ META_CAMPAIGN_CONFIG_INVALID: "Campaign configuration is invalid. Verify `dailyBudgetUsd`, `targetCountries`, and `campaignName`.",
729
+ META_WHATSAPP_DESTINATION_DISABLED: "WhatsApp destination is not enabled for this Meta account. Change the ad destination or enable WhatsApp Business on the account.",
730
730
  // TikTok
731
- TIKTOK_NOT_CONNECTED: "No tienes TikTok Ads conectado. Con\xE9ctalo en https://productmaker.app/integrations.",
732
- TIKTOK_TOKEN_EXPIRED: "La sesi\xF3n con TikTok expir\xF3. Recon\xE9ctalo en https://productmaker.app/integrations.",
733
- TIKTOK_PERMISSION_DENIED: "Tu conexi\xF3n de TikTok no tiene los permisos suficientes. Recon\xE9ctala y acepta todos los permisos solicitados.",
734
- TIKTOK_MISSING_PAYMENT: "Tu cuenta de TikTok Ads no tiene m\xE9todo de pago configurado. Agrega uno en https://ads.tiktok.com.",
735
- TIKTOK_PIXEL_NOT_FOUND: "El Pixel de TikTok no existe o no est\xE1 asociado al advertiser. Verifica el `pixelId`.",
736
- TIKTOK_INVALID_ADVERTISER: "El advertiser no existe o no tienes acceso. Confirma el `advertiserId` con `list_tiktok_advertisers`.",
737
- TIKTOK_ADVERTISER_REQUIRED: "Falta el `advertiserId`. Obt\xE9n uno con `list_tiktok_advertisers` antes de publicar.",
738
- TIKTOK_IDENTITY_REQUIRED: "TikTok requiere una identidad (perfil del anunciante) para publicar. Crea una en https://ads.tiktok.com.",
739
- TIKTOK_IDENTITY_API_REQUIRED: "TikTok requiere habilitar la API de identidades en tu cuenta. Contacta a TikTok Ads Support.",
740
- TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rechaz\xF3 la subida del video. Verifica que el formato sea MP4 < 500MB.",
741
- TIKTOK_VIDEO_UPLOAD_TIMEOUT: "La subida del video a TikTok se demor\xF3 demasiado. Reintenta en unos minutos.",
742
- TIKTOK_RATE_LIMITED: "TikTok limit\xF3 las solicitudes. Espera unos minutos antes de reintentar.",
743
- TIKTOK_SERVER_ERROR: "TikTok devolvi\xF3 un error de servidor. Reintenta en unos minutos.",
744
- TIKTOK_INVALID_DATA: "TikTok rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica `advertiserId`, `pixelId` y `campaignConfig`.",
731
+ TIKTOK_NOT_CONNECTED: "TikTok Ads is not connected. Connect it at https://productmaker.app/integrations.",
732
+ TIKTOK_TOKEN_EXPIRED: "The TikTok session expired. Reconnect it at https://productmaker.app/integrations.",
733
+ TIKTOK_PERMISSION_DENIED: "The TikTok connection lacks sufficient permissions. Reconnect it and accept all requested permissions.",
734
+ TIKTOK_MISSING_PAYMENT: "Your TikTok Ads account has no payment method configured. Add one at https://ads.tiktok.com.",
735
+ TIKTOK_PIXEL_NOT_FOUND: "TikTok Pixel not found or not associated with the advertiser. Verify the `pixelId`.",
736
+ TIKTOK_INVALID_ADVERTISER: "Advertiser not found or no access. Confirm the `advertiserId` with `list_tiktok_advertisers`.",
737
+ TIKTOK_ADVERTISER_REQUIRED: "Missing `advertiserId`. Obtain one with `list_tiktok_advertisers` before publishing.",
738
+ TIKTOK_IDENTITY_REQUIRED: "TikTok requires an identity (advertiser profile) to publish. Create one at https://ads.tiktok.com.",
739
+ TIKTOK_IDENTITY_API_REQUIRED: "TikTok requires the identities API to be enabled on this account. Contact TikTok Ads Support.",
740
+ TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rejected the video upload. Verify the format is MP4 under 500MB.",
741
+ TIKTOK_VIDEO_UPLOAD_TIMEOUT: "The video upload to TikTok timed out. Retry in a few minutes.",
742
+ TIKTOK_RATE_LIMITED: "TikTok rate-limited the request. Wait a few minutes before retrying.",
743
+ TIKTOK_SERVER_ERROR: "TikTok returned a server error. Retry in a few minutes.",
744
+ TIKTOK_INVALID_DATA: "TikTok rejected the request due to invalid data. Verify `advertiserId`, `pixelId`, and `campaignConfig`.",
745
745
  // Campaign (shared)
746
- CAMPAIGN_NOT_FOUND: "La campa\xF1a no existe o no pertenece a este usuario. Verifica el `campaignId`.",
747
- CAMPAIGN_TASK_NOT_FOUND: "La tarea de la campa\xF1a no existe. Verifica el `taskId`.",
748
- CAMPAIGN_MISSING_LANDING_URL: "Falta la URL de la landing en la campa\xF1a. Publica primero a Shopify o pasa `landingUrl` expl\xEDcitamente.",
749
- CAMPAIGN_NO_CREATIVES: "La tarea no tiene creativos (video/imagen) listos para la campa\xF1a. Espera a que termine la generaci\xF3n.",
750
- CAMPAIGN_MISSING_THUMBNAILS: "Faltan miniaturas para los videos. Verifica que la generaci\xF3n de imagen/video haya terminado.",
746
+ CAMPAIGN_NOT_FOUND: "Campaign not found or does not belong to this user. Verify the `campaignId`.",
747
+ CAMPAIGN_TASK_NOT_FOUND: "Campaign task not found. Verify the `taskId`.",
748
+ CAMPAIGN_MISSING_LANDING_URL: "Missing landing URL for the campaign. Publish to Shopify first or pass `landingUrl` explicitly.",
749
+ CAMPAIGN_NO_CREATIVES: "The task has no creatives (video/image) ready for the campaign. Wait for generation to finish.",
750
+ CAMPAIGN_MISSING_THUMBNAILS: "Missing thumbnails for the videos. Verify that image/video generation has finished.",
751
751
  // Connections
752
- NO_SHOPIFY_SHOPS: "No hay tiendas Shopify conectadas. Conecta una en https://productmaker.app/integrations.",
753
- NO_META_AD_ACCOUNTS: "No hay cuentas de Meta Ads conectadas. Conecta Facebook en https://productmaker.app/integrations.",
752
+ NO_SHOPIFY_SHOPS: "No Shopify stores connected. Connect one at https://productmaker.app/integrations.",
753
+ NO_META_AD_ACCOUNTS: "No Meta Ads accounts connected. Connect Facebook at https://productmaker.app/integrations.",
754
754
  // Task lifecycle
755
- TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
756
- TASK_NOT_READY: "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n. Espera a que termine y reintenta.",
755
+ TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
756
+ TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
757
757
  // API key
758
- INVALID_API_KEY: "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.",
759
- REVOKED_API_KEY: "API key revocada. Genera una nueva en https://productmaker.app/account/api-keys.",
758
+ INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
759
+ REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
760
760
  // Rate
761
- RATE_LIMITED: "Demasiadas solicitudes. Espera unos segundos.",
761
+ RATE_LIMITED: "Too many requests. Wait a few seconds.",
762
762
  // Generic
763
- UPSTREAM_UNAVAILABLE: "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.",
764
- UPSTREAM_TIMEOUT: "Tiempo de espera agotado. Reintenta en un momento.",
765
- INTERNAL: "Error interno.",
766
- INVALID_INPUT: "Entrada inv\xE1lida."
763
+ UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
764
+ UPSTREAM_TIMEOUT: "Request timed out. Retry shortly.",
765
+ INTERNAL: "Internal error.",
766
+ INVALID_INPUT: "Invalid input."
767
767
  };
768
768
  var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
769
769
  // Input/auth
@@ -903,11 +903,11 @@ function resolveMessage(code, backendMessage) {
903
903
  const mapped = CODE_MESSAGES[code];
904
904
  const backend = typeof backendMessage === "string" ? backendMessage.trim() : "";
905
905
  if (mapped && backend && looksLikeMeaningfulBackendMessage(backend, mapped)) {
906
- return `${mapped} (Detalle: ${backend})`;
906
+ return `${mapped} (Detail: ${backend})`;
907
907
  }
908
908
  if (mapped) return mapped;
909
909
  if (backend) return backend;
910
- return `El servicio devolvi\xF3 un error (c\xF3digo: ${code}). Intenta de nuevo en un momento o contacta soporte si persiste.`;
910
+ return `The service returned an error (code: ${code}). Try again shortly or contact support if it persists.`;
911
911
  }
912
912
  __name(resolveMessage, "resolveMessage");
913
913
  function translateError(err) {
@@ -1502,7 +1502,7 @@ var RegenerateVideoScriptInput = z4.object({
1502
1502
  narrativeStyle: NarrativeStyleEnum.describe('Narrativa para el nuevo guion (mismos valores que angleNarratives). REQUERIDO \u2014 el backend NO acepta "mantener la actual"; siempre tienes que elegir una. Si el usuario quiere re-rolear con la misma narrativa, pasa la que ya tiene el \xE1ngulo (la ves en get_video_script o en get_task_status).')
1503
1503
  }).strict();
1504
1504
  function registerScriptReviewTools(server, api) {
1505
- server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId? }] }.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
1505
+ server.tool("get_video_script", 'Lee el guion del video pendiente de revisi\xF3n. S\xF3lo funciona cuando la tarea fue creada con `pauseForVideoScriptReview: true` (via generate_video_creative). Devuelve { status, angleKey, perClipSeconds?, scenes: [{ index, dialogue, maxWords, minWords?, beatId? }], error? }. El campo `error` (cuando viene con status="ready") es el motivo legible por el que un intento previo de aprobaci\xF3n fue rechazado (p.ej. el guion no cab\xEDa en el tiempo y el \xE1ngulo volvi\xF3 a "ready"); mu\xE9straselo al usuario.\n\nCU\xC1NDO LLAMAR: no existe un status top-level "awaiting_script_review" en get_task_status. Si la tarea fue creada con pauseForVideoScriptReview=true, polea get_task_status hasta que `status === "processing"` y a partir de ah\xED llama este tool peri\xF3dicamente \u2014 la respuesta de get_video_script trae su propio campo `status` que es la se\xF1al real:\n- "ready" \u2192 el guion est\xE1 listo; mu\xE9strale al usuario cada `scenes[].dialogue` por `index` con su tope `maxWords`, recoge ediciones, llama approve_video_script.\n- "not_ready" \u2192 el guion a\xFAn se est\xE1 generando; espera 5\u201310s y reintenta este tool.\n- "approved" \u2192 ya fue aprobado; en multi-angle puede ser que ese \xE1ngulo ya est\xE9 hecho \u2014 vuelve a llamar con otro angleKey.\n- "skipped" \u2192 el usuario opt\xF3 por no revisar en una pausa previa.\n- "error" \u2192 el backend no pudo generar el guion; usa `regenerate_video_script` para re-intentar (es la ruta de recuperaci\xF3n v\xE1lida \u2014 el guion no se aprueba en estado "error").\n\nMULTI-ANGLE: si la tarea tiene varios \xE1ngulos, omitir angleKey siempre devuelve "angle_0" literal \u2014 itera expl\xEDcitamente por "angle_0", "angle_1", ... hasta encontrar uno con status="ready".', GetVideoScriptInput.shape, async (input) => {
1506
1506
  try {
1507
1507
  const qs = input.angleKey ? `?angleKey=${encodeURIComponent(input.angleKey)}` : "";
1508
1508
  const r = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/video-script${qs}`);
@@ -1522,7 +1522,7 @@ function registerScriptReviewTools(server, api) {
1522
1522
  throw translateError(e);
1523
1523
  }
1524
1524
  });
1525
- server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO las valida si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas). Si quieres que pasen tal cual, simplemente no las incluyas en scenes[].\n4. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n5. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
1525
+ server.tool("approve_video_script", 'Aprueba el guion del video (con o sin ediciones). Tras llamarlo, el render arranca y consume cr\xE9ditos.\n\nREGLAS DURAS:\n1. INCLUYE EN scenes[] S\xD3LO las escenas que cambian. El backend mergea por `index` \u2014 las escenas no incluidas conservan su di\xE1logo original. scenes vac\xEDo es rechazado con 400, as\xED que si el usuario no quiere editar nada, manda AL MENOS UNA escena con su `dialogue` original (ser\xE1 un no-op sem\xE1ntico pero satisface el min(1)).\n2. Para cada entrada incluida cuyo `dialogue` difiera del original, el backend valida `minWords \u2264 words(dialogue) \u2264 maxWords` (los rangos vienen en get_video_script). Si te sales del rango, rechaza con 400 \u2014 NO recortes en silencio; p\xEDdele al usuario que ajuste.\n3. Escenas con LLM-original ya fuera de rango: el backend NO valida el rango POR-ESCENA si no las incluyes (la regla #2 s\xF3lo aplica a escenas editadas).\n4. TOPE GLOBAL: adem\xE1s del rango por-escena, el backend rechaza con 400 ("El guion no cabe en el tiempo del video. Ac\xF3rtalo y vuelve a aprobar.") cuando el TOTAL de palabras de TODO el guion (incluidas las escenas que no editaste) supera el cupo de la voz \u2014 porque ese guion se pasar\xEDa del tope de duraci\xF3n al renderizar. Si lo ves, acorta las escenas m\xE1s largas (la suma de `maxWords` de get_video_script es el techo total) y reintenta; o usa regenerate_video_script. Este 400 es s\xEDncrono y NO consume render.\n5. NUNCA inventes ni reescribas el guion por tu cuenta. Aplica los cambios del usuario al pie de la letra dentro de los l\xEDmites de palabras.\n6. MULTI-ANGLE: pasa el mismo `angleKey` que usaste en get_video_script. Si lo omites, el backend resuelve a "angle_0" literal \u2014 puedes aprobar un \xE1ngulo equivocado.\n\nDevuelve { status }. status="approved" confirma la aprobaci\xF3n; otro valor significa que la pausa sigue activa.', ApproveVideoScriptInput.shape, async (input) => {
1526
1526
  try {
1527
1527
  const body = {
1528
1528
  scenes: input.scenes
@@ -1598,7 +1598,11 @@ var MetaInput = z6.object({
1598
1598
  /** Select which angles to publish by 0-based index. Omit to publish all
1599
1599
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1600
1600
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1601
- campaignConfig: McpCampaignConfig.optional()
1601
+ campaignConfig: McpCampaignConfig.optional(),
1602
+ /** Override the ad's primary text (body copy). If omitted, ProductMaker
1603
+ * uses the auto-generated copy attached to the task. Capped at 500 chars
1604
+ * (Meta's primary-text limit). */
1605
+ primaryText: z6.string().trim().min(1).max(500).optional()
1602
1606
  });
1603
1607
  var TikTokInput = z6.object({
1604
1608
  taskId: z6.string().min(1),
@@ -1606,7 +1610,10 @@ var TikTokInput = z6.object({
1606
1610
  pixelId: z6.string().min(1).optional(),
1607
1611
  landingUrl: z6.string().url().optional(),
1608
1612
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1609
- campaignConfig: McpCampaignConfig.optional()
1613
+ campaignConfig: McpCampaignConfig.optional(),
1614
+ /** Override the ad's description (TikTok's body copy). If omitted,
1615
+ * ProductMaker uses the auto-generated copy attached to the task. */
1616
+ primaryText: z6.string().trim().min(1).max(500).optional()
1610
1617
  });
1611
1618
  function registerPublishTools(server, api) {
1612
1619
  server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto publica TODAS las im\xE1genes que la tarea gener\xF3 para la landing; pasa `numLandingImages` s\xF3lo si el usuario te pide menos. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
@@ -1631,7 +1638,7 @@ function registerPublishTools(server, api) {
1631
1638
  throw translateError(e);
1632
1639
  }
1633
1640
  });
1634
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1641
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1635
1642
  try {
1636
1643
  const body = {
1637
1644
  ...rest
@@ -1639,6 +1646,7 @@ function registerPublishTools(server, api) {
1639
1646
  if (campaignConfig) {
1640
1647
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1641
1648
  }
1649
+ if (primaryText) body.primaryText = primaryText;
1642
1650
  const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1643
1651
  return asText({
1644
1652
  ...r,
@@ -1648,7 +1656,7 @@ function registerPublishTools(server, api) {
1648
1656
  throw translateError(e);
1649
1657
  }
1650
1658
  });
1651
- server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1659
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1652
1660
  try {
1653
1661
  const body = {
1654
1662
  ...rest
@@ -1656,6 +1664,7 @@ function registerPublishTools(server, api) {
1656
1664
  if (campaignConfig) {
1657
1665
  body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1658
1666
  }
1667
+ if (primaryText) body.primaryText = primaryText;
1659
1668
  const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1660
1669
  return asText({
1661
1670
  ...r,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Product Maker MCP server — connect Claude Desktop, Claude Code, ChatGPT, or Gemini to Product Maker AI to generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from your AI assistant.",