@productmaker/mcp 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -153,7 +153,7 @@ var ApiClient = class {
153
153
  };
154
154
 
155
155
  // src/index.ts
156
- var MCP_VERSION = "2.9.1";
156
+ var MCP_VERSION = "2.9.3";
157
157
 
158
158
  // src/meta-upload-app.ts
159
159
  import { readFileSync } from "fs";
@@ -8052,7 +8052,7 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
8052
8052
  {
8053
8053
  name: "publish_creatives_to_meta",
8054
8054
  confirmation: "external_mutation",
8055
- schemaHash: "59b3d6d5ce25812fee3382b0b1c0cf5e07beb1063048dd0a0d0633686cf29da5"
8055
+ schemaHash: "f712fe12baee19de6ac5ed94b23de630247a345ed13c3a431a647d421480280f"
8056
8056
  },
8057
8057
  {
8058
8058
  name: "publish_to_meta",
@@ -8915,7 +8915,7 @@ var CODE_MESSAGES = {
8915
8915
  META_TOKEN_EXPIRED: "Your Meta session expired. Reconnect Facebook at https://productmaker.app/integrations.",
8916
8916
  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.",
8917
8917
  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).",
8918
- META_PERMISSION_DENIED: "The Facebook connection lacks sufficient permissions. Reconnect it at https://productmaker.app/integrations and accept all permissions.",
8918
+ META_PERMISSION_DENIED: "Meta denied write access to the selected ad account. Ask a Business Manager admin to grant this user Advertiser role or higher for that account, then reconnect Facebook at https://productmaker.app/integrations and accept every requested permission, including ads_management.",
8919
8919
  META_PIXEL_NOT_FOUND: "Pixel not found or not associated with the ad account. Verify the `pixelId` at https://business.facebook.com/events_manager.",
8920
8920
  META_OBJECT_NOT_FOUND: "The requested object (campaign/ad set/ad) does not exist in Meta. Verify the ID or create a new one.",
8921
8921
  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.",
@@ -10901,10 +10901,10 @@ var GroupedCreative = z52.object({
10901
10901
  cta: MetaCtaType.optional(),
10902
10902
  creativeId: z52.string().min(1).optional()
10903
10903
  });
10904
- var GroupedCampaignConfig = z52.object({
10904
+ var GroupedCampaignConfigBase = z52.object({
10905
10905
  campaignType: MetaCampaignType.default("sales"),
10906
10906
  optimizationMode: MetaOptimizationMode.default("advantage_plus"),
10907
- budgetMode: MetaBudgetMode.default("campaign"),
10907
+ budgetMode: MetaBudgetMode.optional(),
10908
10908
  campaignDailyBudget: z52.number().int().positive().optional(),
10909
10909
  adSets: z52.array(z52.object({
10910
10910
  tempId: z52.string().min(1).max(80),
@@ -10930,8 +10930,17 @@ var GroupedCampaignConfig = z52.object({
10930
10930
  ]),
10931
10931
  saveAsDefault: z52.boolean().default(false),
10932
10932
  budgetPolicy: MetaBudgetPolicy.optional()
10933
- }).superRefine((value, ctx) => {
10934
- if (value.budgetMode === "campaign" && value.campaignDailyBudget === void 0) {
10933
+ });
10934
+ function resolveBudgetMode(value) {
10935
+ if (value.budgetMode) return value.budgetMode;
10936
+ if (value.campaignDailyBudget !== void 0) return "campaign";
10937
+ if (value.adSets.every((adSet) => adSet.dailyBudget !== void 0)) return "ad_set";
10938
+ return "campaign";
10939
+ }
10940
+ __name(resolveBudgetMode, "resolveBudgetMode");
10941
+ var GroupedCampaignConfig = GroupedCampaignConfigBase.superRefine((value, ctx) => {
10942
+ const budgetMode = resolveBudgetMode(value);
10943
+ if (budgetMode === "campaign" && value.campaignDailyBudget === void 0) {
10935
10944
  ctx.addIssue({
10936
10945
  code: z52.ZodIssueCode.custom,
10937
10946
  path: [
@@ -10940,7 +10949,7 @@ var GroupedCampaignConfig = z52.object({
10940
10949
  message: "campaignDailyBudget is required when budgetMode is campaign"
10941
10950
  });
10942
10951
  }
10943
- if (value.budgetMode === "ad_set") {
10952
+ if (budgetMode === "ad_set") {
10944
10953
  value.adSets.forEach((adSet, index) => {
10945
10954
  if (adSet.dailyBudget === void 0) {
10946
10955
  ctx.addIssue({
@@ -10955,7 +10964,10 @@ var GroupedCampaignConfig = z52.object({
10955
10964
  }
10956
10965
  });
10957
10966
  }
10958
- });
10967
+ }).transform((value) => ({
10968
+ ...value,
10969
+ budgetMode: resolveBudgetMode(value)
10970
+ }));
10959
10971
  var PublishCreativesToMetaInputBase = z52.object({
10960
10972
  creatives: z52.array(GroupedCreative).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
10961
10973
  /** Exact Meta Business portfolio selected from `list_meta_destinations`. */
@@ -10973,7 +10985,7 @@ var PublishCreativesToMetaInputBase = z52.object({
10973
10985
  });
10974
10986
  var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((value, ctx) => {
10975
10987
  const destination = value.campaignConfig?.destination;
10976
- if (destination?.type !== "whatsapp" && !value.landingUrl) {
10988
+ if (destination?.type !== "whatsapp" && destination?.type !== "landing" && !value.landingUrl) {
10977
10989
  ctx.addIssue({
10978
10990
  code: z52.ZodIssueCode.custom,
10979
10991
  path: [
@@ -10982,7 +10994,7 @@ var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((v
10982
10994
  message: "landingUrl is required unless campaignConfig.destination.type is whatsapp"
10983
10995
  });
10984
10996
  }
10985
- if (destination?.type === "landing" && destination.landingUrl !== value.landingUrl) {
10997
+ if (destination?.type === "landing" && value.landingUrl && destination.landingUrl !== value.landingUrl) {
10986
10998
  ctx.addIssue({
10987
10999
  code: z52.ZodIssueCode.custom,
10988
11000
  path: [
@@ -10993,6 +11005,15 @@ var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((v
10993
11005
  message: "campaignConfig.destination.landingUrl must match landingUrl"
10994
11006
  });
10995
11007
  }
11008
+ }).transform((value) => {
11009
+ const destination = value.campaignConfig?.destination;
11010
+ if (destination?.type === "landing" && !value.landingUrl) {
11011
+ return {
11012
+ ...value,
11013
+ landingUrl: destination.landingUrl
11014
+ };
11015
+ }
11016
+ return value;
10996
11017
  });
10997
11018
  var PresignUploadResponseSchema = z52.object({
10998
11019
  uploadUrl: z52.string().url().refine((value) => {
@@ -11104,7 +11125,7 @@ function registerPublishTools(server, api, opts = {}) {
11104
11125
  throw translateError(e);
11105
11126
  }
11106
11127
  });
11107
- server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Antes llama `list_meta_destinations` y pide al usuario elegir el `portfolioId`, `adAccountId` y `pageId` exactos. Para archivos en la laptop con MCP remoto: llama `request_meta_media_upload`, espera al usuario y consulta `check_meta_media_upload`; por CADA entrada de `assets[]` crea un creativo `source.type="staged"`, con `type` igual a su `mediaKind`. Si la carga fue parcial, publica s\xF3lo los assets que ese estado confirm\xF3; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se suben temporalmente al CDN de ProductMaker y se eliminan despu\xE9s de publicar correctamente; si falla, quedan s\xF3lo para reintento y el TTL los limpia. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (rawInput) => {
11128
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Antes llama `list_meta_destinations` y pide al usuario elegir el `portfolioId`, `adAccountId` y `pageId` exactos. Para archivos en la laptop con MCP remoto: llama `request_meta_media_upload`, espera al usuario y consulta `check_meta_media_upload`; por CADA entrada de `assets[]` crea un creativo `source.type="staged"`, con `type` igual a su `mediaKind`. Si la carga fue parcial, publica s\xF3lo los assets que ese estado confirm\xF3; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se suben temporalmente al CDN de ProductMaker y se eliminan despu\xE9s de publicar correctamente; si falla, quedan s\xF3lo durante el TTL para reintento: nunca afirmes que una sesi\xF3n no expira. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (rawInput) => {
11108
11129
  try {
11109
11130
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
11110
11131
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -11377,7 +11398,7 @@ __name(uploadSessionText, "uploadSessionText");
11377
11398
  function registerMetaUploadTools(server, api) {
11378
11399
  server.registerTool("request_meta_media_upload", {
11379
11400
  title: "Solicitar carga segura de Meta",
11380
- description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El flujo principal es el MCP App inline; el enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. Despu\xE9s conserva el `sessionId` y llama `check_meta_media_upload`. Esta herramienta no publica ni consume cr\xE9ditos.",
11401
+ description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El App es exclusivamente para cargar archivos: nunca debe pedir ni mostrar portafolio, cuenta publicitaria, p\xE1gina, IDs o JSON. El enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. Conserva privadamente el `sessionId`; cuando el usuario termine, llama `check_meta_media_upload`, luego `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres el `sessionId`, `stagingAssetId` ni un handoff JSON al usuario. Esta herramienta no publica ni consume cr\xE9ditos.",
11381
11402
  inputSchema: MetaUploadSessionToolInputShape,
11382
11403
  outputSchema: MetaUploadSessionResponse2,
11383
11404
  _meta: {
@@ -11410,7 +11431,7 @@ function registerMetaUploadTools(server, api) {
11410
11431
  }
11411
11432
  });
11412
11433
  server.registerTool("check_meta_media_upload", {
11413
- description: 'Comprueba si el usuario ya termin\xF3 de subir desde el enlace de ProductMaker. Si status=uploaded, devuelve `assets[]`; crea un creativo `source.type="staged"` por cada asset, usando su `mediaKind` como el tipo del creativo, hasta diez en el batch. Si la carga fue parcial, publica \xFAnicamente los assets que `assets[]` reporta como subidos. Conserva el `stagingAssetId` legado si aparece. Si status=pending, pide al usuario que termine; no publiques todav\xEDa.',
11434
+ description: 'Comprueba si el usuario ya termin\xF3 de subir desde el App o enlace de ProductMaker. Si status=uploaded, conserva `assets[]` privadamente, llama `list_meta_destinations` y pregunta en lenguaje natural dentro del chat qu\xE9 portafolio, cuenta publicitaria y p\xE1gina desea usar; nunca muestres `sessionId`, `stagingAssetId`, IDs t\xE9cnicos ni un bloque JSON. Despu\xE9s crea un creativo `source.type="staged"` por cada asset, usando su `mediaKind` como tipo, hasta diez en el batch. Si la carga fue parcial, usa \xFAnicamente los assets confirmados. Conserva el `stagingAssetId` legado si aparece. Si status=pending, pide al usuario que termine; no publiques todav\xEDa.',
11414
11435
  inputSchema: MetaUploadStatusInput.shape,
11415
11436
  _meta: {
11416
11437
  "openai/widgetAccessible": true
package/dist/stdio.js CHANGED
@@ -150,7 +150,7 @@ var ApiClient = class {
150
150
  };
151
151
 
152
152
  // src/index.ts
153
- var MCP_VERSION = "2.9.1";
153
+ var MCP_VERSION = "2.9.3";
154
154
 
155
155
  // src/meta-upload-app.ts
156
156
  import { readFileSync } from "fs";
@@ -8049,7 +8049,7 @@ var PRODUCTMAKER_MCP_CAPABILITIES = [
8049
8049
  {
8050
8050
  name: "publish_creatives_to_meta",
8051
8051
  confirmation: "external_mutation",
8052
- schemaHash: "59b3d6d5ce25812fee3382b0b1c0cf5e07beb1063048dd0a0d0633686cf29da5"
8052
+ schemaHash: "f712fe12baee19de6ac5ed94b23de630247a345ed13c3a431a647d421480280f"
8053
8053
  },
8054
8054
  {
8055
8055
  name: "publish_to_meta",
@@ -8912,7 +8912,7 @@ var CODE_MESSAGES = {
8912
8912
  META_TOKEN_EXPIRED: "Your Meta session expired. Reconnect Facebook at https://productmaker.app/integrations.",
8913
8913
  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.",
8914
8914
  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).",
8915
- META_PERMISSION_DENIED: "The Facebook connection lacks sufficient permissions. Reconnect it at https://productmaker.app/integrations and accept all permissions.",
8915
+ META_PERMISSION_DENIED: "Meta denied write access to the selected ad account. Ask a Business Manager admin to grant this user Advertiser role or higher for that account, then reconnect Facebook at https://productmaker.app/integrations and accept every requested permission, including ads_management.",
8916
8916
  META_PIXEL_NOT_FOUND: "Pixel not found or not associated with the ad account. Verify the `pixelId` at https://business.facebook.com/events_manager.",
8917
8917
  META_OBJECT_NOT_FOUND: "The requested object (campaign/ad set/ad) does not exist in Meta. Verify the ID or create a new one.",
8918
8918
  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.",
@@ -10857,10 +10857,10 @@ var GroupedCreative = z52.object({
10857
10857
  cta: MetaCtaType.optional(),
10858
10858
  creativeId: z52.string().min(1).optional()
10859
10859
  });
10860
- var GroupedCampaignConfig = z52.object({
10860
+ var GroupedCampaignConfigBase = z52.object({
10861
10861
  campaignType: MetaCampaignType.default("sales"),
10862
10862
  optimizationMode: MetaOptimizationMode.default("advantage_plus"),
10863
- budgetMode: MetaBudgetMode.default("campaign"),
10863
+ budgetMode: MetaBudgetMode.optional(),
10864
10864
  campaignDailyBudget: z52.number().int().positive().optional(),
10865
10865
  adSets: z52.array(z52.object({
10866
10866
  tempId: z52.string().min(1).max(80),
@@ -10886,8 +10886,17 @@ var GroupedCampaignConfig = z52.object({
10886
10886
  ]),
10887
10887
  saveAsDefault: z52.boolean().default(false),
10888
10888
  budgetPolicy: MetaBudgetPolicy.optional()
10889
- }).superRefine((value, ctx) => {
10890
- if (value.budgetMode === "campaign" && value.campaignDailyBudget === void 0) {
10889
+ });
10890
+ function resolveBudgetMode(value) {
10891
+ if (value.budgetMode) return value.budgetMode;
10892
+ if (value.campaignDailyBudget !== void 0) return "campaign";
10893
+ if (value.adSets.every((adSet) => adSet.dailyBudget !== void 0)) return "ad_set";
10894
+ return "campaign";
10895
+ }
10896
+ __name(resolveBudgetMode, "resolveBudgetMode");
10897
+ var GroupedCampaignConfig = GroupedCampaignConfigBase.superRefine((value, ctx) => {
10898
+ const budgetMode = resolveBudgetMode(value);
10899
+ if (budgetMode === "campaign" && value.campaignDailyBudget === void 0) {
10891
10900
  ctx.addIssue({
10892
10901
  code: z52.ZodIssueCode.custom,
10893
10902
  path: [
@@ -10896,7 +10905,7 @@ var GroupedCampaignConfig = z52.object({
10896
10905
  message: "campaignDailyBudget is required when budgetMode is campaign"
10897
10906
  });
10898
10907
  }
10899
- if (value.budgetMode === "ad_set") {
10908
+ if (budgetMode === "ad_set") {
10900
10909
  value.adSets.forEach((adSet, index) => {
10901
10910
  if (adSet.dailyBudget === void 0) {
10902
10911
  ctx.addIssue({
@@ -10911,7 +10920,10 @@ var GroupedCampaignConfig = z52.object({
10911
10920
  }
10912
10921
  });
10913
10922
  }
10914
- });
10923
+ }).transform((value) => ({
10924
+ ...value,
10925
+ budgetMode: resolveBudgetMode(value)
10926
+ }));
10915
10927
  var PublishCreativesToMetaInputBase = z52.object({
10916
10928
  creatives: z52.array(GroupedCreative).min(1).max(CAMPAIGN_LIMITS.MAX_CREATIVES),
10917
10929
  /** Exact Meta Business portfolio selected from `list_meta_destinations`. */
@@ -10929,7 +10941,7 @@ var PublishCreativesToMetaInputBase = z52.object({
10929
10941
  });
10930
10942
  var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((value, ctx) => {
10931
10943
  const destination = value.campaignConfig?.destination;
10932
- if (destination?.type !== "whatsapp" && !value.landingUrl) {
10944
+ if (destination?.type !== "whatsapp" && destination?.type !== "landing" && !value.landingUrl) {
10933
10945
  ctx.addIssue({
10934
10946
  code: z52.ZodIssueCode.custom,
10935
10947
  path: [
@@ -10938,7 +10950,7 @@ var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((v
10938
10950
  message: "landingUrl is required unless campaignConfig.destination.type is whatsapp"
10939
10951
  });
10940
10952
  }
10941
- if (destination?.type === "landing" && destination.landingUrl !== value.landingUrl) {
10953
+ if (destination?.type === "landing" && value.landingUrl && destination.landingUrl !== value.landingUrl) {
10942
10954
  ctx.addIssue({
10943
10955
  code: z52.ZodIssueCode.custom,
10944
10956
  path: [
@@ -10949,6 +10961,15 @@ var PublishCreativesToMetaInput = PublishCreativesToMetaInputBase.superRefine((v
10949
10961
  message: "campaignConfig.destination.landingUrl must match landingUrl"
10950
10962
  });
10951
10963
  }
10964
+ }).transform((value) => {
10965
+ const destination = value.campaignConfig?.destination;
10966
+ if (destination?.type === "landing" && !value.landingUrl) {
10967
+ return {
10968
+ ...value,
10969
+ landingUrl: destination.landingUrl
10970
+ };
10971
+ }
10972
+ return value;
10952
10973
  });
10953
10974
  var PresignUploadResponseSchema = z52.object({
10954
10975
  uploadUrl: z52.string().url().refine((value) => {
@@ -11060,7 +11081,7 @@ function registerPublishTools(server, api, opts = {}) {
11060
11081
  throw translateError(e);
11061
11082
  }
11062
11083
  });
11063
- server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Antes llama `list_meta_destinations` y pide al usuario elegir el `portfolioId`, `adAccountId` y `pageId` exactos. Para archivos en la laptop con MCP remoto: llama `request_meta_media_upload`, espera al usuario y consulta `check_meta_media_upload`; por CADA entrada de `assets[]` crea un creativo `source.type="staged"`, con `type` igual a su `mediaKind`. Si la carga fue parcial, publica s\xF3lo los assets que ese estado confirm\xF3; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se suben temporalmente al CDN de ProductMaker y se eliminan despu\xE9s de publicar correctamente; si falla, quedan s\xF3lo para reintento y el TTL los limpia. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (rawInput) => {
11084
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Antes llama `list_meta_destinations` y pide al usuario elegir el `portfolioId`, `adAccountId` y `pageId` exactos. Para archivos en la laptop con MCP remoto: llama `request_meta_media_upload`, espera al usuario y consulta `check_meta_media_upload`; por CADA entrada de `assets[]` crea un creativo `source.type="staged"`, con `type` igual a su `mediaKind`. Si la carga fue parcial, publica s\xF3lo los assets que ese estado confirm\xF3; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se suben temporalmente al CDN de ProductMaker y se eliminan despu\xE9s de publicar correctamente; si falla, quedan s\xF3lo durante el TTL para reintento: nunca afirmes que una sesi\xF3n no expira. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, async (rawInput) => {
11064
11085
  try {
11065
11086
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
11066
11087
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -11333,7 +11354,7 @@ __name(uploadSessionText, "uploadSessionText");
11333
11354
  function registerMetaUploadTools(server, api) {
11334
11355
  server.registerTool("request_meta_media_upload", {
11335
11356
  title: "Solicitar carga segura de Meta",
11336
- description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El flujo principal es el MCP App inline; el enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. Despu\xE9s conserva el `sessionId` y llama `check_meta_media_upload`. Esta herramienta no publica ni consume cr\xE9ditos.",
11357
+ description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El App es exclusivamente para cargar archivos: nunca debe pedir ni mostrar portafolio, cuenta publicitaria, p\xE1gina, IDs o JSON. El enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. Conserva privadamente el `sessionId`; cuando el usuario termine, llama `check_meta_media_upload`, luego `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres el `sessionId`, `stagingAssetId` ni un handoff JSON al usuario. Esta herramienta no publica ni consume cr\xE9ditos.",
11337
11358
  inputSchema: MetaUploadSessionToolInputShape,
11338
11359
  outputSchema: MetaUploadSessionResponse2,
11339
11360
  _meta: {
@@ -11366,7 +11387,7 @@ function registerMetaUploadTools(server, api) {
11366
11387
  }
11367
11388
  });
11368
11389
  server.registerTool("check_meta_media_upload", {
11369
- description: 'Comprueba si el usuario ya termin\xF3 de subir desde el enlace de ProductMaker. Si status=uploaded, devuelve `assets[]`; crea un creativo `source.type="staged"` por cada asset, usando su `mediaKind` como el tipo del creativo, hasta diez en el batch. Si la carga fue parcial, publica \xFAnicamente los assets que `assets[]` reporta como subidos. Conserva el `stagingAssetId` legado si aparece. Si status=pending, pide al usuario que termine; no publiques todav\xEDa.',
11390
+ description: 'Comprueba si el usuario ya termin\xF3 de subir desde el App o enlace de ProductMaker. Si status=uploaded, conserva `assets[]` privadamente, llama `list_meta_destinations` y pregunta en lenguaje natural dentro del chat qu\xE9 portafolio, cuenta publicitaria y p\xE1gina desea usar; nunca muestres `sessionId`, `stagingAssetId`, IDs t\xE9cnicos ni un bloque JSON. Despu\xE9s crea un creativo `source.type="staged"` por cada asset, usando su `mediaKind` como tipo, hasta diez en el batch. Si la carga fue parcial, usa \xFAnicamente los assets confirmados. Conserva el `stagingAssetId` legado si aparece. Si status=pending, pide al usuario que termine; no publiques todav\xEDa.',
11370
11391
  inputSchema: MetaUploadStatusInput.shape,
11371
11392
  _meta: {
11372
11393
  "openai/widgetAccessible": true
@@ -66,21 +66,16 @@
66
66
  }
67
67
 
68
68
  button,
69
- input,
70
- select,
71
- textarea {
69
+ input {
72
70
  font: inherit;
73
71
  }
74
72
 
75
73
  button,
76
- select,
77
74
  a {
78
75
  -webkit-tap-highlight-color: transparent;
79
76
  }
80
77
 
81
78
  button:focus-visible,
82
- select:focus-visible,
83
- textarea:focus-visible,
84
79
  a:focus-visible,
85
80
  [tabindex]:focus-visible {
86
81
  outline: 3px solid
@@ -102,73 +97,12 @@
102
97
  box-shadow: var(--pm-shadow);
103
98
  }
104
99
 
105
- .hero {
106
- padding: 19px 22px 14px;
107
- border-bottom: 1px solid
108
- color-mix(in srgb, var(--pm-line) 65%, transparent);
109
- background:
110
- radial-gradient(
111
- circle at 100% 0%,
112
- var(--pm-primary-soft),
113
- transparent 48%
114
- ),
115
- var(--pm-card);
116
- }
117
-
118
- .upload-header {
119
- display: flex;
120
- align-items: center;
121
- justify-content: space-between;
122
- gap: 12px;
123
- }
124
-
125
- .upload-title {
126
- min-width: 0;
127
- margin: 0;
128
- overflow: hidden;
129
- font-size: clamp(17px, 3vw, 24px);
130
- font-weight: 760;
131
- letter-spacing: -0.025em;
132
- text-overflow: ellipsis;
133
- white-space: nowrap;
134
- }
135
-
136
- .brand-mark {
137
- display: grid;
138
- width: 34px;
139
- height: 34px;
140
- flex: 0 0 auto;
141
- place-items: center;
142
- border: 1px solid
143
- color-mix(in srgb, var(--pm-primary) 20%, var(--pm-line));
144
- border-radius: 50%;
145
- background: color-mix(
146
- in srgb,
147
- var(--pm-primary-soft) 70%,
148
- var(--pm-card)
149
- );
150
- color: var(--pm-primary-strong);
151
- font-size: 20px;
152
- font-weight: 800;
153
- line-height: 1;
154
- }
155
-
156
- .session-row {
100
+ .upload-feedback {
157
101
  display: flex;
158
- min-height: 0;
159
102
  align-items: center;
103
+ flex-wrap: wrap;
160
104
  gap: 10px;
161
- margin-top: 8px;
162
- }
163
-
164
- .session-id {
165
- overflow: hidden;
166
- max-width: 100%;
167
- color: var(--pm-muted);
168
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
169
- font-size: 10px;
170
- text-overflow: ellipsis;
171
- white-space: nowrap;
105
+ margin-top: 10px;
172
106
  }
173
107
 
174
108
  .fallback-link {
@@ -212,20 +146,23 @@
212
146
  }
213
147
 
214
148
  .section {
215
- padding: 12px 22px 22px;
149
+ padding: 18px;
216
150
  }
217
151
 
218
- .section + .section {
219
- border-top: 1px solid var(--pm-line);
152
+ .media-grid {
153
+ display: grid;
154
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
155
+ gap: 12px;
156
+ align-items: start;
220
157
  }
221
158
 
222
159
  .dropzone {
223
160
  display: grid;
224
- min-height: 260px;
161
+ aspect-ratio: 1;
162
+ min-width: 0;
225
163
  place-items: center;
226
- padding: 20px;
227
164
  border: 2px dashed var(--pm-line);
228
- border-radius: 21px;
165
+ border-radius: 16px;
229
166
  background: color-mix(
230
167
  in srgb,
231
168
  var(--pm-card-muted) 62%,
@@ -251,13 +188,11 @@
251
188
 
252
189
  .drop-trigger {
253
190
  display: grid;
254
- width: min(100%, 280px);
255
- min-height: 190px;
191
+ width: 100%;
192
+ height: 100%;
256
193
  place-items: center;
257
- align-content: center;
258
- gap: 16px;
259
194
  border: 0;
260
- border-radius: 18px;
195
+ border-radius: 14px;
261
196
  background: transparent;
262
197
  color: var(--pm-text);
263
198
  cursor: pointer;
@@ -269,26 +204,19 @@
269
204
 
270
205
  .drop-icon {
271
206
  display: grid;
272
- width: 58px;
273
- height: 58px;
207
+ width: 48px;
208
+ height: 48px;
274
209
  place-items: center;
275
210
  border: 1px solid var(--pm-line);
276
211
  border-radius: 50%;
277
212
  background: var(--pm-card);
278
213
  box-shadow: 0 7px 20px rgb(27 36 66 / 8%);
279
214
  color: var(--pm-text);
280
- font-size: 31px;
215
+ font-size: 29px;
281
216
  font-weight: 350;
282
217
  line-height: 1;
283
218
  }
284
219
 
285
- .drop-title {
286
- margin: 0;
287
- font-size: 18px;
288
- font-weight: 520;
289
- letter-spacing: -0.02em;
290
- }
291
-
292
220
  .drop-help {
293
221
  position: absolute;
294
222
  width: 1px;
@@ -309,47 +237,6 @@
309
237
  clip-path: inset(50%);
310
238
  }
311
239
 
312
- .button {
313
- display: inline-flex;
314
- min-height: 42px;
315
- align-items: center;
316
- justify-content: center;
317
- gap: 8px;
318
- padding: 10px 16px;
319
- border: 1px solid transparent;
320
- border-radius: 12px;
321
- background: var(--pm-primary);
322
- color: #ffffff;
323
- cursor: pointer;
324
- font-size: 13px;
325
- font-weight: 780;
326
- line-height: 1;
327
- transition:
328
- background 140ms ease,
329
- opacity 140ms ease,
330
- transform 140ms ease;
331
- }
332
-
333
- .button:hover:not(:disabled) {
334
- background: var(--pm-primary-strong);
335
- transform: translateY(-1px);
336
- }
337
-
338
- .button:disabled {
339
- cursor: not-allowed;
340
- opacity: 0.5;
341
- }
342
-
343
- .button.secondary {
344
- border-color: var(--pm-line);
345
- background: var(--pm-card);
346
- color: var(--pm-text);
347
- }
348
-
349
- .button.secondary:hover:not(:disabled) {
350
- background: var(--pm-card-muted);
351
- }
352
-
353
240
  .batch-error {
354
241
  display: none;
355
242
  margin: 10px 0 0;
@@ -362,10 +249,7 @@
362
249
  }
363
250
 
364
251
  .file-list {
365
- display: grid;
366
- grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
367
- gap: 12px;
368
- margin-top: 14px;
252
+ display: contents;
369
253
  }
370
254
 
371
255
  .file-card {
@@ -501,63 +385,11 @@
501
385
  transition: width 120ms linear;
502
386
  }
503
387
 
504
- .destination-grid {
505
- display: grid;
506
- grid-template-columns: repeat(3, minmax(0, 1fr));
507
- gap: 9px;
508
- }
509
-
510
- .field {
511
- display: grid;
512
- }
513
-
514
- select,
515
- textarea {
516
- width: 100%;
517
- border: 1px solid var(--pm-line);
518
- border-radius: 11px;
519
- background: var(--pm-card);
520
- color: var(--pm-text);
521
- }
522
-
523
- select {
524
- min-height: 42px;
525
- padding: 8px 30px 8px 10px;
526
- font-size: 12px;
527
- }
528
-
529
- textarea {
530
- min-height: 135px;
531
- padding: 12px;
532
- resize: vertical;
533
- font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
534
- font-size: 11px;
535
- line-height: 1.5;
536
- }
537
-
538
- .destination-actions,
539
- .handoff-actions {
540
- display: flex;
541
- flex-wrap: wrap;
542
- gap: 10px;
543
- margin-top: 12px;
544
- }
545
-
546
388
  .compatibility-control {
389
+ /* Retained for older hosts; recovery is visible only through the secure fallback. */
547
390
  display: none !important;
548
391
  }
549
392
 
550
- .sr-only {
551
- position: absolute;
552
- width: 1px;
553
- height: 1px;
554
- padding: 0;
555
- overflow: hidden;
556
- clip: rect(0, 0, 0, 0);
557
- white-space: nowrap;
558
- border: 0;
559
- }
560
-
561
393
  [hidden] {
562
394
  display: none !important;
563
395
  }
@@ -571,23 +403,13 @@
571
403
  border-radius: 20px;
572
404
  }
573
405
 
574
- .hero,
575
406
  .section {
576
407
  padding-right: 14px;
577
408
  padding-left: 14px;
578
409
  }
579
410
 
580
- .dropzone {
581
- min-height: 230px;
582
- }
583
-
584
- .destination-grid {
585
- grid-template-columns: 1fr;
586
- }
587
-
588
- .destination-actions .button,
589
- .handoff-actions .button {
590
- width: 100%;
411
+ .media-grid {
412
+ grid-template-columns: repeat(2, minmax(0, 1fr));
591
413
  }
592
414
  }
593
415
 
@@ -604,71 +426,46 @@
604
426
  <body>
605
427
  <main class="app" data-testid="meta-upload-root">
606
428
  <article class="card">
607
- <header class="hero">
608
- <div class="upload-header">
609
- <p class="upload-title" data-testid="meta-upload-title">
610
- Upload image
611
- </p>
612
- <span class="brand-mark" aria-label="ProductMaker" role="img"
613
- >↗</span
614
- >
615
- </div>
616
- <div class="session-row">
617
- <code class="session-id" data-testid="meta-upload-session-id" hidden
618
- >Esperando sesión…</code
619
- >
620
- <a
621
- class="fallback-link"
622
- data-testid="meta-upload-fallback-link"
623
- href="#"
624
- target="_blank"
625
- rel="noopener noreferrer"
626
- hidden
627
- >Abrir carga segura en otra pestaña</a
628
- >
629
- </div>
630
- <p
631
- class="status"
632
- data-testid="meta-upload-session-status"
633
- aria-live="polite"
634
- ></p>
635
- </header>
636
-
637
429
  <section class="section" aria-label="Upload media">
638
- <div
639
- class="dropzone"
640
- data-testid="meta-upload-dropzone"
641
- data-dragging="false"
642
- aria-disabled="true"
643
- aria-describedby="meta-upload-constraints"
644
- >
645
- <button
646
- class="drop-trigger"
647
- type="button"
648
- data-testid="meta-upload-picker-btn"
649
- disabled
430
+ <div class="media-grid">
431
+ <div
432
+ class="file-list"
433
+ data-testid="meta-upload-file-list"
434
+ aria-live="polite"
435
+ ></div>
436
+ <div
437
+ class="dropzone"
438
+ data-testid="meta-upload-dropzone"
439
+ data-dragging="false"
440
+ aria-disabled="true"
441
+ aria-describedby="meta-upload-constraints"
650
442
  >
651
- <span class="drop-icon" aria-hidden="true">+</span>
652
- <span class="drop-title" data-testid="meta-upload-drop-title"
653
- >Upload image</span
443
+ <button
444
+ class="drop-trigger"
445
+ type="button"
446
+ data-testid="meta-upload-add-tile"
447
+ aria-label="Agregar otro archivo"
448
+ disabled
654
449
  >
655
- </button>
656
- <button
657
- class="compatibility-control"
658
- type="button"
659
- data-testid="meta-upload-library-btn"
660
- hidden
661
- disabled
662
- aria-label="Choose from ChatGPT library"
663
- ></button>
664
- <input
665
- class="native-input"
666
- data-testid="meta-upload-input"
667
- type="file"
668
- accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
669
- multiple
670
- disabled
671
- />
450
+ <span class="drop-icon" aria-hidden="true">+</span>
451
+ </button>
452
+ <button
453
+ class="compatibility-control"
454
+ type="button"
455
+ data-testid="meta-upload-library-btn"
456
+ hidden
457
+ disabled
458
+ aria-label="Choose from ChatGPT library"
459
+ ></button>
460
+ <input
461
+ class="native-input"
462
+ data-testid="meta-upload-input"
463
+ type="file"
464
+ accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
465
+ multiple
466
+ disabled
467
+ />
468
+ </div>
672
469
  </div>
673
470
  <p class="drop-help" id="meta-upload-constraints">
674
471
  JPEG, PNG, WebP, MP4, MOV o WebM. Hasta 10 archivos de 350 MB; las
@@ -680,90 +477,21 @@
680
477
  role="alert"
681
478
  aria-live="assertive"
682
479
  ></p>
683
- <div
684
- class="file-list"
685
- data-testid="meta-upload-file-list"
686
- aria-live="polite"
687
- ></div>
688
- <button
689
- class="compatibility-control"
690
- type="button"
691
- data-testid="meta-upload-pending-refresh"
692
- hidden
693
- aria-label="Refresh upload status"
694
- ></button>
695
- </section>
696
-
697
- <section
698
- class="section"
699
- data-testid="meta-upload-destination-section"
700
- aria-label="Meta destination"
701
- hidden
702
- >
703
- <div class="destination-grid">
704
- <div class="field">
705
- <label class="sr-only" for="portfolio-select">Portfolio</label>
706
- <select
707
- id="portfolio-select"
708
- data-testid="meta-upload-portfolio-select"
709
- >
710
- <option value="">Portfolio</option>
711
- </select>
712
- </div>
713
- <div class="field">
714
- <label class="sr-only" for="ad-account-select">Ad account</label>
715
- <select
716
- id="ad-account-select"
717
- data-testid="meta-upload-ad-account-select"
718
- disabled
719
- >
720
- <option value="">Ad account</option>
721
- </select>
722
- </div>
723
- <div class="field">
724
- <label class="sr-only" for="page-select">Facebook Page</label>
725
- <select id="page-select" data-testid="meta-upload-page-select">
726
- <option value="">Facebook Page</option>
727
- </select>
728
- </div>
729
- </div>
730
- <div class="destination-actions">
731
- <button
732
- class="button"
733
- type="button"
734
- data-testid="meta-upload-submit-destination"
735
- disabled
736
- >
737
- Continue
738
- </button>
739
- <button
740
- class="compatibility-control"
741
- type="button"
742
- data-testid="meta-upload-check-status"
480
+ <div class="upload-feedback">
481
+ <a
482
+ class="fallback-link"
483
+ data-testid="meta-upload-fallback-link"
484
+ href="#"
485
+ target="_blank"
486
+ rel="noopener noreferrer"
743
487
  hidden
744
- aria-label="Refresh upload status"
745
- ></button>
746
- </div>
747
- </section>
748
-
749
- <section
750
- class="section"
751
- data-testid="meta-upload-handoff-section"
752
- aria-labelledby="handoff-heading"
753
- hidden
754
- >
755
- <p id="handoff-heading" class="section-copy muted">
756
- Copy this handoff into the chat.
757
- </p>
758
- <textarea data-testid="meta-upload-handoff-copy" readonly></textarea>
759
- <div class="handoff-actions">
760
- <button
761
- class="button secondary"
762
- type="button"
763
- data-testid="meta-upload-copy-btn"
488
+ >Abrir carga segura en otra pestaña</a
764
489
  >
765
- Copy handoff
766
- </button>
490
+ <p
491
+ class="status"
492
+ data-testid="meta-upload-session-status"
493
+ aria-live="polite"
494
+ ></p>
767
495
  </div>
768
496
  </section>
769
497
  </article>
@@ -801,10 +529,6 @@
801
529
 
802
530
  const elements = {
803
531
  root: document.querySelector('[data-testid="meta-upload-root"]'),
804
- title: document.querySelector('[data-testid="meta-upload-title"]'),
805
- sessionId: document.querySelector(
806
- '[data-testid="meta-upload-session-id"]',
807
- ),
808
532
  fallbackLink: document.querySelector(
809
533
  '[data-testid="meta-upload-fallback-link"]',
810
534
  ),
@@ -814,11 +538,8 @@
814
538
  dropzone: document.querySelector(
815
539
  '[data-testid="meta-upload-dropzone"]',
816
540
  ),
817
- dropTitle: document.querySelector(
818
- '[data-testid="meta-upload-drop-title"]',
819
- ),
820
541
  pickerButton: document.querySelector(
821
- '[data-testid="meta-upload-picker-btn"]',
542
+ '[data-testid="meta-upload-add-tile"]',
822
543
  ),
823
544
  libraryPickerButton: document.querySelector(
824
545
  '[data-testid="meta-upload-library-btn"]',
@@ -830,36 +551,6 @@
830
551
  fileList: document.querySelector(
831
552
  '[data-testid="meta-upload-file-list"]',
832
553
  ),
833
- destinationSection: document.querySelector(
834
- '[data-testid="meta-upload-destination-section"]',
835
- ),
836
- portfolio: document.querySelector(
837
- '[data-testid="meta-upload-portfolio-select"]',
838
- ),
839
- adAccount: document.querySelector(
840
- '[data-testid="meta-upload-ad-account-select"]',
841
- ),
842
- page: document.querySelector(
843
- '[data-testid="meta-upload-page-select"]',
844
- ),
845
- submitDestination: document.querySelector(
846
- '[data-testid="meta-upload-submit-destination"]',
847
- ),
848
- checkStatus: document.querySelector(
849
- '[data-testid="meta-upload-check-status"]',
850
- ),
851
- pendingRefresh: document.querySelector(
852
- '[data-testid="meta-upload-pending-refresh"]',
853
- ),
854
- handoffSection: document.querySelector(
855
- '[data-testid="meta-upload-handoff-section"]',
856
- ),
857
- handoffCopy: document.querySelector(
858
- '[data-testid="meta-upload-handoff-copy"]',
859
- ),
860
- copyButton: document.querySelector(
861
- '[data-testid="meta-upload-copy-btn"]',
862
- ),
863
554
  };
864
555
 
865
556
  const state = {
@@ -878,7 +569,8 @@
878
569
  toolInput: {},
879
570
  items: [],
880
571
  assets: [],
881
- destinations: { portfolios: [], adAccounts: [], pages: [] },
572
+ completionNotified: false,
573
+ completionNotificationInFlight: false,
882
574
  consumedBridgeIds: new Set(),
883
575
  consumingBridgeFiles: false,
884
576
  bridgeConsumptionQueued: false,
@@ -926,29 +618,6 @@
926
618
  return null;
927
619
  }
928
620
 
929
- function defaultUploadTitle() {
930
- return state.session?.mediaKind === "video"
931
- ? "Upload video"
932
- : "Upload image";
933
- }
934
-
935
- function updateUploadTitle() {
936
- const namedItems = state.items.filter(
937
- (item) =>
938
- (item.file && typeof item.file.name === "string") ||
939
- (typeof item.displayName === "string" &&
940
- item.displayName.length > 0),
941
- );
942
- const title =
943
- namedItems.length === 1
944
- ? namedItems[0].file?.name || namedItems[0].displayName
945
- : namedItems.length > 1
946
- ? `${namedItems.length} files`
947
- : defaultUploadTitle();
948
- elements.title.textContent = title;
949
- elements.dropTitle.textContent = defaultUploadTitle();
950
- }
951
-
952
621
  function safeSessionLimit() {
953
622
  const proposed =
954
623
  state.session && Number.isInteger(state.session.maxFiles)
@@ -1281,32 +950,6 @@
1281
950
  void processLocalFiles(files);
1282
951
  });
1283
952
 
1284
- elements.portfolio.addEventListener("change", () => {
1285
- renderAdAccounts();
1286
- updateDestinationSubmit();
1287
- });
1288
- elements.adAccount.addEventListener(
1289
- "change",
1290
- updateDestinationSubmit,
1291
- );
1292
- elements.page.addEventListener("change", updateDestinationSubmit);
1293
- elements.submitDestination.addEventListener(
1294
- "click",
1295
- () => void submitDestination(),
1296
- );
1297
- elements.checkStatus.addEventListener(
1298
- "click",
1299
- () => void refreshSessionStatus(),
1300
- );
1301
- elements.pendingRefresh.addEventListener(
1302
- "click",
1303
- () => void refreshSessionStatus(),
1304
- );
1305
- elements.copyButton.addEventListener(
1306
- "click",
1307
- () => void copyHandoff(),
1308
- );
1309
-
1310
953
  state.hostMessageHandler = handleHostMessage;
1311
954
  window.addEventListener("message", state.hostMessageHandler);
1312
955
  }
@@ -1440,26 +1083,24 @@
1440
1083
  if (sessionChanged) {
1441
1084
  state.uploadBlocked = false;
1442
1085
  state.fallbackRequired = false;
1086
+ state.completionNotified = false;
1087
+ state.completionNotificationInFlight = false;
1443
1088
  state.workCancelled = false;
1444
1089
  if (state.abortController?.signal.aborted) {
1445
1090
  state.abortController = null;
1446
1091
  }
1447
1092
  }
1448
1093
  state.session = session;
1449
- elements.sessionId.textContent = session.sessionId;
1450
1094
  elements.fallbackLink.href = session.uploadPageUrl;
1451
- updateUploadTitle();
1452
1095
  if (sessionExpired()) {
1453
1096
  requireFallback(
1454
1097
  "La sesión está expirada. Abre el enlace seguro para solicitar una nueva.",
1455
1098
  "danger",
1456
1099
  true,
1457
1100
  );
1458
- elements.pendingRefresh.hidden = true;
1459
1101
  } else {
1460
1102
  setFallbackVisibility(state.fallbackRequired);
1461
1103
  setSessionStatus("Sesión lista para recibir archivos.", "neutral");
1462
- elements.pendingRefresh.hidden = true;
1463
1104
  }
1464
1105
  setPickerAvailability();
1465
1106
  void consumePendingBridgeFiles();
@@ -2444,7 +2085,6 @@
2444
2085
  fragment.append(card);
2445
2086
  });
2446
2087
  elements.fileList.replaceChildren(fragment);
2447
- updateUploadTitle();
2448
2088
  scheduleIntrinsicSizeReport(true);
2449
2089
  }
2450
2090
 
@@ -2458,8 +2098,6 @@
2458
2098
 
2459
2099
  async function refreshSessionStatus() {
2460
2100
  if (!state.session) return;
2461
- elements.checkStatus.disabled = true;
2462
- elements.pendingRefresh.disabled = true;
2463
2101
  setSessionStatus("Comprobando el staging privado…", "neutral");
2464
2102
  try {
2465
2103
  const result = await callReadOnlyTool("check_meta_media_upload", {
@@ -2474,17 +2112,13 @@
2474
2112
  "warning",
2475
2113
  );
2476
2114
  } else {
2477
- setFallbackVisibility(state.fallbackRequired);
2478
- setSessionStatus(
2479
- "La sesión sigue pendiente. Termina la carga o vuelve a comprobar.",
2115
+ requireFallback(
2116
+ "La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
2480
2117
  "warning",
2481
2118
  );
2482
2119
  }
2483
- elements.destinationSection.hidden = true;
2484
- elements.pendingRefresh.hidden = true;
2485
2120
  return;
2486
2121
  }
2487
- elements.pendingRefresh.hidden = true;
2488
2122
 
2489
2123
  const partial =
2490
2124
  status.status === "pending" ||
@@ -2501,15 +2135,10 @@
2501
2135
  setSessionStatus(
2502
2136
  partial
2503
2137
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2504
- : "Carga verificada. Elige ahora el destino de Meta.",
2138
+ : "Carga completada.",
2505
2139
  partial ? "warning" : "success",
2506
2140
  );
2507
- const destinationsResult = await callReadOnlyTool(
2508
- "list_meta_destinations",
2509
- {},
2510
- );
2511
- state.destinations = parseDestinationsResult(destinationsResult);
2512
- renderDestinations();
2141
+ await notifyChatUploadComplete();
2513
2142
  } catch (error) {
2514
2143
  const expired =
2515
2144
  sessionExpired() ||
@@ -2522,20 +2151,11 @@
2522
2151
  ? "host_tools_unavailable: este host no permite comprobar el staging desde la App. Usa el enlace seguro."
2523
2152
  : "No se pudo comprobar la sesión. Conserva el enlace y vuelve a intentarlo.";
2524
2153
  requireFallback(message, "danger", expired);
2525
- elements.pendingRefresh.hidden = true;
2526
- } finally {
2527
- elements.checkStatus.disabled = false;
2528
- elements.pendingRefresh.hidden = true;
2529
- elements.pendingRefresh.disabled = false;
2530
2154
  }
2531
2155
  }
2532
2156
 
2533
2157
  async function callReadOnlyTool(name, args) {
2534
- if (
2535
- !["check_meta_media_upload", "list_meta_destinations"].includes(
2536
- name,
2537
- )
2538
- ) {
2158
+ if (name !== "check_meta_media_upload") {
2539
2159
  throw new Error("tool_not_allowed");
2540
2160
  }
2541
2161
  const bridge = openAiBridge();
@@ -2626,173 +2246,43 @@
2626
2246
  return { status, assets, legacyKindAmbiguous };
2627
2247
  }
2628
2248
 
2629
- function parseDestinationsResult(result) {
2630
- const payload = resultPayload(result);
2631
- return {
2632
- portfolios: parseDestinationList(payload.portfolios, (entry) => ({
2633
- id: boundedString(entry.id, 300),
2634
- name: boundedString(entry.name, 300) || "Portfolio sin nombre",
2635
- })),
2636
- adAccounts: parseDestinationList(payload.adAccounts, (entry) => ({
2637
- id: boundedString(entry.id, 300),
2638
- name: boundedString(entry.name, 300) || "Cuenta sin nombre",
2639
- currency: boundedString(entry.currency, 20),
2640
- portfolioId: boundedString(entry.portfolioId, 300),
2641
- accountStatus: Number(entry.accountStatus),
2642
- })).filter(
2643
- (entry) => entry.portfolioId && entry.accountStatus === 1,
2644
- ),
2645
- pages: parseDestinationList(payload.pages, (entry) => ({
2646
- id: boundedString(entry.id, 300),
2647
- name: boundedString(entry.name, 300) || "Página sin nombre",
2648
- })),
2649
- };
2650
- }
2651
-
2652
- function parseDestinationList(value, mapper) {
2653
- if (!Array.isArray(value)) return [];
2654
- return value.slice(0, 1000).flatMap((entry) => {
2655
- if (!isRecord(entry)) return [];
2656
- const mapped = mapper(entry);
2657
- return mapped.id ? [mapped] : [];
2658
- });
2659
- }
2660
-
2661
- function renderDestinations() {
2662
- fillSelect(
2663
- elements.portfolio,
2664
- state.destinations.portfolios,
2665
- "Portfolio",
2666
- (entry) => entry.name,
2667
- );
2668
- fillSelect(
2669
- elements.page,
2670
- state.destinations.pages,
2671
- "Facebook Page",
2672
- (entry) => entry.name,
2673
- );
2674
- renderAdAccounts();
2675
- elements.destinationSection.hidden = false;
2676
- updateDestinationSubmit();
2677
- scheduleIntrinsicSizeReport(true);
2678
- }
2679
-
2680
- function renderAdAccounts() {
2681
- const portfolioId = elements.portfolio.value;
2682
- const accounts = portfolioId
2683
- ? state.destinations.adAccounts.filter(
2684
- (account) => account.portfolioId === portfolioId,
2685
- )
2686
- : [];
2687
- fillSelect(
2688
- elements.adAccount,
2689
- accounts,
2690
- portfolioId ? "Ad account" : "Ad account",
2691
- (entry) =>
2692
- `${entry.name}${entry.currency ? ` · ${entry.currency}` : ""}`,
2693
- );
2694
- elements.adAccount.disabled = !portfolioId || !accounts.length;
2695
- }
2696
-
2697
- function fillSelect(select, entries, placeholder, labelFor) {
2698
- const fragment = document.createDocumentFragment();
2699
- const empty = document.createElement("option");
2700
- empty.value = "";
2701
- empty.textContent = placeholder;
2702
- fragment.append(empty);
2703
- for (const entry of entries) {
2704
- const option = document.createElement("option");
2705
- option.value = entry.id;
2706
- option.textContent = labelFor(entry);
2707
- fragment.append(option);
2708
- }
2709
- select.replaceChildren(fragment);
2710
- select.value = "";
2711
- }
2712
-
2713
- function updateDestinationSubmit() {
2714
- elements.submitDestination.disabled = !(
2715
- elements.portfolio.value &&
2716
- elements.adAccount.value &&
2717
- elements.page.value &&
2718
- state.assets.length
2719
- );
2720
- }
2721
-
2722
- function exactHandoff() {
2723
- return JSON.stringify({
2724
- sessionId: state.session.sessionId,
2725
- portfolioId: elements.portfolio.value,
2726
- adAccountId: elements.adAccount.value,
2727
- pageId: elements.page.value,
2728
- assets: state.assets.slice(0, MAX_FILES).map((asset) => ({
2729
- stagingAssetId: asset.stagingAssetId,
2730
- mediaKind: asset.mediaKind,
2731
- })),
2732
- });
2733
- }
2734
-
2735
- async function submitDestination() {
2736
- if (elements.submitDestination.disabled || !state.session) return;
2737
- const handoff = exactHandoff();
2738
- elements.submitDestination.disabled = true;
2249
+ async function notifyChatUploadComplete() {
2250
+ if (
2251
+ state.completionNotified ||
2252
+ state.completionNotificationInFlight ||
2253
+ !state.session
2254
+ )
2255
+ return;
2256
+ state.completionNotificationInFlight = true;
2257
+ const message =
2258
+ "Terminé de subir los archivos. Verifica esta misma sesión de carga y continúa en el chat: muéstrame las opciones disponibles y pregúntame por el portafolio, la cuenta publicitaria y la página de Meta. No publiques nada todavía.";
2739
2259
  try {
2740
2260
  const bridge = openAiBridge();
2741
2261
  if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2742
- await bridge.sendFollowUpMessage({ prompt: handoff });
2743
- setSessionStatus(
2744
- "Selección enviada al chat. Espera la confirmación antes de continuar.",
2745
- "success",
2746
- );
2262
+ await bridge.sendFollowUpMessage({ prompt: message });
2263
+ state.completionNotified = true;
2747
2264
  return;
2748
2265
  }
2749
2266
  if (state.hostCanMessage) {
2750
2267
  await rpcRequest("ui/message", {
2751
2268
  role: "user",
2752
- content: [{ type: "text", text: handoff }],
2269
+ content: [{ type: "text", text: message }],
2753
2270
  });
2754
- setSessionStatus(
2755
- "Selección enviada al chat. Espera la confirmación antes de continuar.",
2756
- "success",
2757
- );
2271
+ state.completionNotified = true;
2758
2272
  return;
2759
2273
  }
2760
- showCopyableHandoff(handoff);
2274
+ setSessionStatus(
2275
+ "Carga completada. Escribe “listo” en el chat para continuar.",
2276
+ "success",
2277
+ );
2761
2278
  } catch {
2762
- showCopyableHandoff(handoff);
2279
+ setSessionStatus(
2280
+ "Carga completada. Escribe “listo” en el chat para continuar.",
2281
+ "success",
2282
+ );
2763
2283
  } finally {
2764
- updateDestinationSubmit();
2765
- }
2766
- }
2767
-
2768
- function showCopyableHandoff(handoff) {
2769
- elements.handoffCopy.value = handoff;
2770
- elements.handoffSection.hidden = false;
2771
- setSessionStatus(
2772
- "Copia el handoff exacto y pégalo en el chat.",
2773
- "warning",
2774
- );
2775
- scheduleIntrinsicSizeReport(true);
2776
- }
2777
-
2778
- async function copyHandoff() {
2779
- const handoff = elements.handoffCopy.value;
2780
- if (!handoff) return;
2781
- if (
2782
- navigator.clipboard &&
2783
- typeof navigator.clipboard.writeText === "function"
2784
- ) {
2785
- try {
2786
- await navigator.clipboard.writeText(handoff);
2787
- elements.copyButton.textContent = "Copiado";
2788
- return;
2789
- } catch {
2790
- // Selection fallback below.
2791
- }
2284
+ state.completionNotificationInFlight = false;
2792
2285
  }
2793
- elements.handoffCopy.focus();
2794
- elements.handoffCopy.select();
2795
- elements.copyButton.textContent = "Seleccionado para copiar";
2796
2286
  }
2797
2287
 
2798
2288
  document.addEventListener("DOMContentLoaded", initialize, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "2.9.1",
3
+ "version": "2.9.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Product Maker MCP server — cost products, generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from Claude, ChatGPT, or Gemini.",
@@ -64,9 +64,9 @@
64
64
  "tsx": "^4.16.0",
65
65
  "typescript": "^7.0.2",
66
66
  "vitest": "^2.1.9",
67
+ "@packages/product-economics": "0.1.0",
67
68
  "@packages/product-research": "0.1.0",
68
- "@packages/types": "0.1.0",
69
- "@packages/product-economics": "0.1.0"
69
+ "@packages/types": "0.1.0"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public"