@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.
- package/dist/http.js +74 -65
- package/dist/stdio.js +74 -65
- 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
|
|
706
|
-
SHOPIFY_TOKEN_EXPIRED: "
|
|
707
|
-
SHOPIFY_SCOPE_UPGRADE_REQUIRED: "
|
|
708
|
-
SHOPIFY_PUBLISH_FAILED: "Shopify
|
|
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
|
|
711
|
-
META_NO_ADS_CREATED: "No
|
|
712
|
-
META_NO_ASSETS_READY: "
|
|
713
|
-
META_NO_THUMBNAIL_SOURCE: "No
|
|
714
|
-
META_NO_LANDING_URL: "
|
|
715
|
-
META_TASK_NOT_FOUND: "
|
|
716
|
-
META_INVALID_DATA: "Meta
|
|
717
|
-
META_INVALID_AD_ACCOUNT: "
|
|
718
|
-
META_MISSING_PAYMENT: "
|
|
719
|
-
META_TOKEN_EXPIRED: "
|
|
720
|
-
META_ACCOUNT_RESTRICTED: "
|
|
721
|
-
META_CAPABILITY_MISSING: "
|
|
722
|
-
META_PERMISSION_DENIED: "
|
|
723
|
-
META_PIXEL_NOT_FOUND: "
|
|
724
|
-
META_OBJECT_NOT_FOUND: "
|
|
725
|
-
META_AI_DISCLOSURE_REQUIRED: "Meta
|
|
726
|
-
META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta
|
|
727
|
-
META_CBO_BUDGET_CONFLICT: "
|
|
728
|
-
META_BUDGET_MODE_CONFLICT: "Meta
|
|
729
|
-
META_AD_SET_BUDGET_REQUIRED: "
|
|
730
|
-
META_DAILY_BUDGET_TOO_HIGH: "
|
|
731
|
-
META_CAMPAIGN_CONFIG_INVALID: "
|
|
732
|
-
META_WHATSAPP_DESTINATION_DISABLED: "
|
|
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: "
|
|
735
|
-
TIKTOK_TOKEN_EXPIRED: "
|
|
736
|
-
TIKTOK_PERMISSION_DENIED: "
|
|
737
|
-
TIKTOK_MISSING_PAYMENT: "
|
|
738
|
-
TIKTOK_PIXEL_NOT_FOUND: "
|
|
739
|
-
TIKTOK_INVALID_ADVERTISER: "
|
|
740
|
-
TIKTOK_ADVERTISER_REQUIRED: "
|
|
741
|
-
TIKTOK_IDENTITY_REQUIRED: "TikTok
|
|
742
|
-
TIKTOK_IDENTITY_API_REQUIRED: "TikTok
|
|
743
|
-
TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok
|
|
744
|
-
TIKTOK_VIDEO_UPLOAD_TIMEOUT: "
|
|
745
|
-
TIKTOK_RATE_LIMITED: "TikTok
|
|
746
|
-
TIKTOK_SERVER_ERROR: "TikTok
|
|
747
|
-
TIKTOK_INVALID_DATA: "TikTok
|
|
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: "
|
|
750
|
-
CAMPAIGN_TASK_NOT_FOUND: "
|
|
751
|
-
CAMPAIGN_MISSING_LANDING_URL: "
|
|
752
|
-
CAMPAIGN_NO_CREATIVES: "
|
|
753
|
-
CAMPAIGN_MISSING_THUMBNAILS: "
|
|
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
|
|
756
|
-
NO_META_AD_ACCOUNTS: "No
|
|
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: "
|
|
759
|
-
TASK_NOT_READY: "
|
|
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
|
|
762
|
-
REVOKED_API_KEY: "API key
|
|
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: "
|
|
764
|
+
RATE_LIMITED: "Too many requests. Wait a few seconds.",
|
|
765
765
|
// Generic
|
|
766
|
-
UPSTREAM_UNAVAILABLE: "
|
|
767
|
-
UPSTREAM_TIMEOUT: "
|
|
768
|
-
INTERNAL: "
|
|
769
|
-
INVALID_INPUT: "
|
|
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} (
|
|
909
|
+
return `${mapped} (Detail: ${backend})`;
|
|
910
910
|
}
|
|
911
911
|
if (mapped) return mapped;
|
|
912
912
|
if (backend) return backend;
|
|
913
|
-
return `
|
|
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
|
|
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
|
|
703
|
-
SHOPIFY_TOKEN_EXPIRED: "
|
|
704
|
-
SHOPIFY_SCOPE_UPGRADE_REQUIRED: "
|
|
705
|
-
SHOPIFY_PUBLISH_FAILED: "Shopify
|
|
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
|
|
708
|
-
META_NO_ADS_CREATED: "No
|
|
709
|
-
META_NO_ASSETS_READY: "
|
|
710
|
-
META_NO_THUMBNAIL_SOURCE: "No
|
|
711
|
-
META_NO_LANDING_URL: "
|
|
712
|
-
META_TASK_NOT_FOUND: "
|
|
713
|
-
META_INVALID_DATA: "Meta
|
|
714
|
-
META_INVALID_AD_ACCOUNT: "
|
|
715
|
-
META_MISSING_PAYMENT: "
|
|
716
|
-
META_TOKEN_EXPIRED: "
|
|
717
|
-
META_ACCOUNT_RESTRICTED: "
|
|
718
|
-
META_CAPABILITY_MISSING: "
|
|
719
|
-
META_PERMISSION_DENIED: "
|
|
720
|
-
META_PIXEL_NOT_FOUND: "
|
|
721
|
-
META_OBJECT_NOT_FOUND: "
|
|
722
|
-
META_AI_DISCLOSURE_REQUIRED: "Meta
|
|
723
|
-
META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta
|
|
724
|
-
META_CBO_BUDGET_CONFLICT: "
|
|
725
|
-
META_BUDGET_MODE_CONFLICT: "Meta
|
|
726
|
-
META_AD_SET_BUDGET_REQUIRED: "
|
|
727
|
-
META_DAILY_BUDGET_TOO_HIGH: "
|
|
728
|
-
META_CAMPAIGN_CONFIG_INVALID: "
|
|
729
|
-
META_WHATSAPP_DESTINATION_DISABLED: "
|
|
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: "
|
|
732
|
-
TIKTOK_TOKEN_EXPIRED: "
|
|
733
|
-
TIKTOK_PERMISSION_DENIED: "
|
|
734
|
-
TIKTOK_MISSING_PAYMENT: "
|
|
735
|
-
TIKTOK_PIXEL_NOT_FOUND: "
|
|
736
|
-
TIKTOK_INVALID_ADVERTISER: "
|
|
737
|
-
TIKTOK_ADVERTISER_REQUIRED: "
|
|
738
|
-
TIKTOK_IDENTITY_REQUIRED: "TikTok
|
|
739
|
-
TIKTOK_IDENTITY_API_REQUIRED: "TikTok
|
|
740
|
-
TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok
|
|
741
|
-
TIKTOK_VIDEO_UPLOAD_TIMEOUT: "
|
|
742
|
-
TIKTOK_RATE_LIMITED: "TikTok
|
|
743
|
-
TIKTOK_SERVER_ERROR: "TikTok
|
|
744
|
-
TIKTOK_INVALID_DATA: "TikTok
|
|
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: "
|
|
747
|
-
CAMPAIGN_TASK_NOT_FOUND: "
|
|
748
|
-
CAMPAIGN_MISSING_LANDING_URL: "
|
|
749
|
-
CAMPAIGN_NO_CREATIVES: "
|
|
750
|
-
CAMPAIGN_MISSING_THUMBNAILS: "
|
|
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
|
|
753
|
-
NO_META_AD_ACCOUNTS: "No
|
|
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: "
|
|
756
|
-
TASK_NOT_READY: "
|
|
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
|
|
759
|
-
REVOKED_API_KEY: "API key
|
|
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: "
|
|
761
|
+
RATE_LIMITED: "Too many requests. Wait a few seconds.",
|
|
762
762
|
// Generic
|
|
763
|
-
UPSTREAM_UNAVAILABLE: "
|
|
764
|
-
UPSTREAM_TIMEOUT: "
|
|
765
|
-
INTERNAL: "
|
|
766
|
-
INVALID_INPUT: "
|
|
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} (
|
|
906
|
+
return `${mapped} (Detail: ${backend})`;
|
|
907
907
|
}
|
|
908
908
|
if (mapped) return mapped;
|
|
909
909
|
if (backend) return backend;
|
|
910
|
-
return `
|
|
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
|
|
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.
|
|
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.",
|