@productmaker/mcp 2.9.2 → 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.2";
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.2";
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 {
100
+ .upload-feedback {
119
101
  display: flex;
120
102
  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 {
157
- display: flex;
158
- min-height: 0;
159
- 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,64 +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 {
547
389
  /* Retained for older hosts; recovery is visible only through the secure fallback. */
548
390
  display: none !important;
549
391
  }
550
392
 
551
- .sr-only {
552
- position: absolute;
553
- width: 1px;
554
- height: 1px;
555
- padding: 0;
556
- overflow: hidden;
557
- clip: rect(0, 0, 0, 0);
558
- white-space: nowrap;
559
- border: 0;
560
- }
561
-
562
393
  [hidden] {
563
394
  display: none !important;
564
395
  }
@@ -572,23 +403,13 @@
572
403
  border-radius: 20px;
573
404
  }
574
405
 
575
- .hero,
576
406
  .section {
577
407
  padding-right: 14px;
578
408
  padding-left: 14px;
579
409
  }
580
410
 
581
- .dropzone {
582
- min-height: 230px;
583
- }
584
-
585
- .destination-grid {
586
- grid-template-columns: 1fr;
587
- }
588
-
589
- .destination-actions .button,
590
- .handoff-actions .button {
591
- width: 100%;
411
+ .media-grid {
412
+ grid-template-columns: repeat(2, minmax(0, 1fr));
592
413
  }
593
414
  }
594
415
 
@@ -605,71 +426,46 @@
605
426
  <body>
606
427
  <main class="app" data-testid="meta-upload-root">
607
428
  <article class="card">
608
- <header class="hero">
609
- <div class="upload-header">
610
- <p class="upload-title" data-testid="meta-upload-title">
611
- Upload image
612
- </p>
613
- <span class="brand-mark" aria-label="ProductMaker" role="img"
614
- >↗</span
615
- >
616
- </div>
617
- <div class="session-row">
618
- <code class="session-id" data-testid="meta-upload-session-id" hidden
619
- >Esperando sesión…</code
620
- >
621
- <a
622
- class="fallback-link"
623
- data-testid="meta-upload-fallback-link"
624
- href="#"
625
- target="_blank"
626
- rel="noopener noreferrer"
627
- hidden
628
- >Abrir carga segura en otra pestaña</a
629
- >
630
- </div>
631
- <p
632
- class="status"
633
- data-testid="meta-upload-session-status"
634
- aria-live="polite"
635
- ></p>
636
- </header>
637
-
638
429
  <section class="section" aria-label="Upload media">
639
- <div
640
- class="dropzone"
641
- data-testid="meta-upload-dropzone"
642
- data-dragging="false"
643
- aria-disabled="true"
644
- aria-describedby="meta-upload-constraints"
645
- >
646
- <button
647
- class="drop-trigger"
648
- type="button"
649
- data-testid="meta-upload-picker-btn"
650
- 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"
651
442
  >
652
- <span class="drop-icon" aria-hidden="true">+</span>
653
- <span class="drop-title" data-testid="meta-upload-drop-title"
654
- >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
655
449
  >
656
- </button>
657
- <button
658
- class="compatibility-control"
659
- type="button"
660
- data-testid="meta-upload-library-btn"
661
- hidden
662
- disabled
663
- aria-label="Choose from ChatGPT library"
664
- ></button>
665
- <input
666
- class="native-input"
667
- data-testid="meta-upload-input"
668
- type="file"
669
- accept="image/jpeg,image/png,image/webp,video/mp4,video/quicktime,video/webm"
670
- multiple
671
- disabled
672
- />
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>
673
469
  </div>
674
470
  <p class="drop-help" id="meta-upload-constraints">
675
471
  JPEG, PNG, WebP, MP4, MOV o WebM. Hasta 10 archivos de 350 MB; las
@@ -681,76 +477,21 @@
681
477
  role="alert"
682
478
  aria-live="assertive"
683
479
  ></p>
684
- <div
685
- class="file-list"
686
- data-testid="meta-upload-file-list"
687
- aria-live="polite"
688
- ></div>
689
- </section>
690
-
691
- <section
692
- class="section"
693
- data-testid="meta-upload-destination-section"
694
- aria-label="Meta destination"
695
- hidden
696
- >
697
- <div class="destination-grid">
698
- <div class="field">
699
- <label class="sr-only" for="portfolio-select">Portfolio</label>
700
- <select
701
- id="portfolio-select"
702
- data-testid="meta-upload-portfolio-select"
703
- >
704
- <option value="">Portfolio</option>
705
- </select>
706
- </div>
707
- <div class="field">
708
- <label class="sr-only" for="ad-account-select">Ad account</label>
709
- <select
710
- id="ad-account-select"
711
- data-testid="meta-upload-ad-account-select"
712
- disabled
713
- >
714
- <option value="">Ad account</option>
715
- </select>
716
- </div>
717
- <div class="field">
718
- <label class="sr-only" for="page-select">Facebook Page</label>
719
- <select id="page-select" data-testid="meta-upload-page-select">
720
- <option value="">Facebook Page</option>
721
- </select>
722
- </div>
723
- </div>
724
- <div class="destination-actions">
725
- <button
726
- class="button"
727
- type="button"
728
- data-testid="meta-upload-submit-destination"
729
- disabled
730
- >
731
- Continue
732
- </button>
733
- </div>
734
- </section>
735
-
736
- <section
737
- class="section"
738
- data-testid="meta-upload-handoff-section"
739
- aria-labelledby="handoff-heading"
740
- hidden
741
- >
742
- <p id="handoff-heading" class="section-copy muted">
743
- Copy this handoff into the chat.
744
- </p>
745
- <textarea data-testid="meta-upload-handoff-copy" readonly></textarea>
746
- <div class="handoff-actions">
747
- <button
748
- class="button secondary"
749
- type="button"
750
- data-testid="meta-upload-copy-btn"
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"
487
+ hidden
488
+ >Abrir carga segura en otra pestaña</a
751
489
  >
752
- Copy handoff
753
- </button>
490
+ <p
491
+ class="status"
492
+ data-testid="meta-upload-session-status"
493
+ aria-live="polite"
494
+ ></p>
754
495
  </div>
755
496
  </section>
756
497
  </article>
@@ -788,10 +529,6 @@
788
529
 
789
530
  const elements = {
790
531
  root: document.querySelector('[data-testid="meta-upload-root"]'),
791
- title: document.querySelector('[data-testid="meta-upload-title"]'),
792
- sessionId: document.querySelector(
793
- '[data-testid="meta-upload-session-id"]',
794
- ),
795
532
  fallbackLink: document.querySelector(
796
533
  '[data-testid="meta-upload-fallback-link"]',
797
534
  ),
@@ -801,11 +538,8 @@
801
538
  dropzone: document.querySelector(
802
539
  '[data-testid="meta-upload-dropzone"]',
803
540
  ),
804
- dropTitle: document.querySelector(
805
- '[data-testid="meta-upload-drop-title"]',
806
- ),
807
541
  pickerButton: document.querySelector(
808
- '[data-testid="meta-upload-picker-btn"]',
542
+ '[data-testid="meta-upload-add-tile"]',
809
543
  ),
810
544
  libraryPickerButton: document.querySelector(
811
545
  '[data-testid="meta-upload-library-btn"]',
@@ -817,30 +551,6 @@
817
551
  fileList: document.querySelector(
818
552
  '[data-testid="meta-upload-file-list"]',
819
553
  ),
820
- destinationSection: document.querySelector(
821
- '[data-testid="meta-upload-destination-section"]',
822
- ),
823
- portfolio: document.querySelector(
824
- '[data-testid="meta-upload-portfolio-select"]',
825
- ),
826
- adAccount: document.querySelector(
827
- '[data-testid="meta-upload-ad-account-select"]',
828
- ),
829
- page: document.querySelector(
830
- '[data-testid="meta-upload-page-select"]',
831
- ),
832
- submitDestination: document.querySelector(
833
- '[data-testid="meta-upload-submit-destination"]',
834
- ),
835
- handoffSection: document.querySelector(
836
- '[data-testid="meta-upload-handoff-section"]',
837
- ),
838
- handoffCopy: document.querySelector(
839
- '[data-testid="meta-upload-handoff-copy"]',
840
- ),
841
- copyButton: document.querySelector(
842
- '[data-testid="meta-upload-copy-btn"]',
843
- ),
844
554
  };
845
555
 
846
556
  const state = {
@@ -859,7 +569,8 @@
859
569
  toolInput: {},
860
570
  items: [],
861
571
  assets: [],
862
- destinations: { portfolios: [], adAccounts: [], pages: [] },
572
+ completionNotified: false,
573
+ completionNotificationInFlight: false,
863
574
  consumedBridgeIds: new Set(),
864
575
  consumingBridgeFiles: false,
865
576
  bridgeConsumptionQueued: false,
@@ -907,29 +618,6 @@
907
618
  return null;
908
619
  }
909
620
 
910
- function defaultUploadTitle() {
911
- return state.session?.mediaKind === "video"
912
- ? "Upload video"
913
- : "Upload image";
914
- }
915
-
916
- function updateUploadTitle() {
917
- const namedItems = state.items.filter(
918
- (item) =>
919
- (item.file && typeof item.file.name === "string") ||
920
- (typeof item.displayName === "string" &&
921
- item.displayName.length > 0),
922
- );
923
- const title =
924
- namedItems.length === 1
925
- ? namedItems[0].file?.name || namedItems[0].displayName
926
- : namedItems.length > 1
927
- ? `${namedItems.length} files`
928
- : defaultUploadTitle();
929
- elements.title.textContent = title;
930
- elements.dropTitle.textContent = defaultUploadTitle();
931
- }
932
-
933
621
  function safeSessionLimit() {
934
622
  const proposed =
935
623
  state.session && Number.isInteger(state.session.maxFiles)
@@ -1262,24 +950,6 @@
1262
950
  void processLocalFiles(files);
1263
951
  });
1264
952
 
1265
- elements.portfolio.addEventListener("change", () => {
1266
- renderAdAccounts();
1267
- updateDestinationSubmit();
1268
- });
1269
- elements.adAccount.addEventListener(
1270
- "change",
1271
- updateDestinationSubmit,
1272
- );
1273
- elements.page.addEventListener("change", updateDestinationSubmit);
1274
- elements.submitDestination.addEventListener(
1275
- "click",
1276
- () => void submitDestination(),
1277
- );
1278
- elements.copyButton.addEventListener(
1279
- "click",
1280
- () => void copyHandoff(),
1281
- );
1282
-
1283
953
  state.hostMessageHandler = handleHostMessage;
1284
954
  window.addEventListener("message", state.hostMessageHandler);
1285
955
  }
@@ -1413,15 +1083,15 @@
1413
1083
  if (sessionChanged) {
1414
1084
  state.uploadBlocked = false;
1415
1085
  state.fallbackRequired = false;
1086
+ state.completionNotified = false;
1087
+ state.completionNotificationInFlight = false;
1416
1088
  state.workCancelled = false;
1417
1089
  if (state.abortController?.signal.aborted) {
1418
1090
  state.abortController = null;
1419
1091
  }
1420
1092
  }
1421
1093
  state.session = session;
1422
- elements.sessionId.textContent = session.sessionId;
1423
1094
  elements.fallbackLink.href = session.uploadPageUrl;
1424
- updateUploadTitle();
1425
1095
  if (sessionExpired()) {
1426
1096
  requireFallback(
1427
1097
  "La sesión está expirada. Abre el enlace seguro para solicitar una nueva.",
@@ -2415,7 +2085,6 @@
2415
2085
  fragment.append(card);
2416
2086
  });
2417
2087
  elements.fileList.replaceChildren(fragment);
2418
- updateUploadTitle();
2419
2088
  scheduleIntrinsicSizeReport(true);
2420
2089
  }
2421
2090
 
@@ -2448,7 +2117,6 @@
2448
2117
  "warning",
2449
2118
  );
2450
2119
  }
2451
- elements.destinationSection.hidden = true;
2452
2120
  return;
2453
2121
  }
2454
2122
 
@@ -2467,15 +2135,10 @@
2467
2135
  setSessionStatus(
2468
2136
  partial
2469
2137
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2470
- : "Carga verificada. Elige ahora el destino de Meta.",
2138
+ : "Carga completada.",
2471
2139
  partial ? "warning" : "success",
2472
2140
  );
2473
- const destinationsResult = await callReadOnlyTool(
2474
- "list_meta_destinations",
2475
- {},
2476
- );
2477
- state.destinations = parseDestinationsResult(destinationsResult);
2478
- renderDestinations();
2141
+ await notifyChatUploadComplete();
2479
2142
  } catch (error) {
2480
2143
  const expired =
2481
2144
  sessionExpired() ||
@@ -2492,11 +2155,7 @@
2492
2155
  }
2493
2156
 
2494
2157
  async function callReadOnlyTool(name, args) {
2495
- if (
2496
- !["check_meta_media_upload", "list_meta_destinations"].includes(
2497
- name,
2498
- )
2499
- ) {
2158
+ if (name !== "check_meta_media_upload") {
2500
2159
  throw new Error("tool_not_allowed");
2501
2160
  }
2502
2161
  const bridge = openAiBridge();
@@ -2587,173 +2246,43 @@
2587
2246
  return { status, assets, legacyKindAmbiguous };
2588
2247
  }
2589
2248
 
2590
- function parseDestinationsResult(result) {
2591
- const payload = resultPayload(result);
2592
- return {
2593
- portfolios: parseDestinationList(payload.portfolios, (entry) => ({
2594
- id: boundedString(entry.id, 300),
2595
- name: boundedString(entry.name, 300) || "Portfolio sin nombre",
2596
- })),
2597
- adAccounts: parseDestinationList(payload.adAccounts, (entry) => ({
2598
- id: boundedString(entry.id, 300),
2599
- name: boundedString(entry.name, 300) || "Cuenta sin nombre",
2600
- currency: boundedString(entry.currency, 20),
2601
- portfolioId: boundedString(entry.portfolioId, 300),
2602
- accountStatus: Number(entry.accountStatus),
2603
- })).filter(
2604
- (entry) => entry.portfolioId && entry.accountStatus === 1,
2605
- ),
2606
- pages: parseDestinationList(payload.pages, (entry) => ({
2607
- id: boundedString(entry.id, 300),
2608
- name: boundedString(entry.name, 300) || "Página sin nombre",
2609
- })),
2610
- };
2611
- }
2612
-
2613
- function parseDestinationList(value, mapper) {
2614
- if (!Array.isArray(value)) return [];
2615
- return value.slice(0, 1000).flatMap((entry) => {
2616
- if (!isRecord(entry)) return [];
2617
- const mapped = mapper(entry);
2618
- return mapped.id ? [mapped] : [];
2619
- });
2620
- }
2621
-
2622
- function renderDestinations() {
2623
- fillSelect(
2624
- elements.portfolio,
2625
- state.destinations.portfolios,
2626
- "Portfolio",
2627
- (entry) => entry.name,
2628
- );
2629
- fillSelect(
2630
- elements.page,
2631
- state.destinations.pages,
2632
- "Facebook Page",
2633
- (entry) => entry.name,
2634
- );
2635
- renderAdAccounts();
2636
- elements.destinationSection.hidden = false;
2637
- updateDestinationSubmit();
2638
- scheduleIntrinsicSizeReport(true);
2639
- }
2640
-
2641
- function renderAdAccounts() {
2642
- const portfolioId = elements.portfolio.value;
2643
- const accounts = portfolioId
2644
- ? state.destinations.adAccounts.filter(
2645
- (account) => account.portfolioId === portfolioId,
2646
- )
2647
- : [];
2648
- fillSelect(
2649
- elements.adAccount,
2650
- accounts,
2651
- portfolioId ? "Ad account" : "Ad account",
2652
- (entry) =>
2653
- `${entry.name}${entry.currency ? ` · ${entry.currency}` : ""}`,
2654
- );
2655
- elements.adAccount.disabled = !portfolioId || !accounts.length;
2656
- }
2657
-
2658
- function fillSelect(select, entries, placeholder, labelFor) {
2659
- const fragment = document.createDocumentFragment();
2660
- const empty = document.createElement("option");
2661
- empty.value = "";
2662
- empty.textContent = placeholder;
2663
- fragment.append(empty);
2664
- for (const entry of entries) {
2665
- const option = document.createElement("option");
2666
- option.value = entry.id;
2667
- option.textContent = labelFor(entry);
2668
- fragment.append(option);
2669
- }
2670
- select.replaceChildren(fragment);
2671
- select.value = "";
2672
- }
2673
-
2674
- function updateDestinationSubmit() {
2675
- elements.submitDestination.disabled = !(
2676
- elements.portfolio.value &&
2677
- elements.adAccount.value &&
2678
- elements.page.value &&
2679
- state.assets.length
2680
- );
2681
- }
2682
-
2683
- function exactHandoff() {
2684
- return JSON.stringify({
2685
- sessionId: state.session.sessionId,
2686
- portfolioId: elements.portfolio.value,
2687
- adAccountId: elements.adAccount.value,
2688
- pageId: elements.page.value,
2689
- assets: state.assets.slice(0, MAX_FILES).map((asset) => ({
2690
- stagingAssetId: asset.stagingAssetId,
2691
- mediaKind: asset.mediaKind,
2692
- })),
2693
- });
2694
- }
2695
-
2696
- async function submitDestination() {
2697
- if (elements.submitDestination.disabled || !state.session) return;
2698
- const handoff = exactHandoff();
2699
- 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.";
2700
2259
  try {
2701
2260
  const bridge = openAiBridge();
2702
2261
  if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2703
- await bridge.sendFollowUpMessage({ prompt: handoff });
2704
- setSessionStatus(
2705
- "Selección enviada al chat. Espera la confirmación antes de continuar.",
2706
- "success",
2707
- );
2262
+ await bridge.sendFollowUpMessage({ prompt: message });
2263
+ state.completionNotified = true;
2708
2264
  return;
2709
2265
  }
2710
2266
  if (state.hostCanMessage) {
2711
2267
  await rpcRequest("ui/message", {
2712
2268
  role: "user",
2713
- content: [{ type: "text", text: handoff }],
2269
+ content: [{ type: "text", text: message }],
2714
2270
  });
2715
- setSessionStatus(
2716
- "Selección enviada al chat. Espera la confirmación antes de continuar.",
2717
- "success",
2718
- );
2271
+ state.completionNotified = true;
2719
2272
  return;
2720
2273
  }
2721
- showCopyableHandoff(handoff);
2274
+ setSessionStatus(
2275
+ "Carga completada. Escribe “listo” en el chat para continuar.",
2276
+ "success",
2277
+ );
2722
2278
  } catch {
2723
- showCopyableHandoff(handoff);
2279
+ setSessionStatus(
2280
+ "Carga completada. Escribe “listo” en el chat para continuar.",
2281
+ "success",
2282
+ );
2724
2283
  } finally {
2725
- updateDestinationSubmit();
2726
- }
2727
- }
2728
-
2729
- function showCopyableHandoff(handoff) {
2730
- elements.handoffCopy.value = handoff;
2731
- elements.handoffSection.hidden = false;
2732
- setSessionStatus(
2733
- "Copia el handoff exacto y pégalo en el chat.",
2734
- "warning",
2735
- );
2736
- scheduleIntrinsicSizeReport(true);
2737
- }
2738
-
2739
- async function copyHandoff() {
2740
- const handoff = elements.handoffCopy.value;
2741
- if (!handoff) return;
2742
- if (
2743
- navigator.clipboard &&
2744
- typeof navigator.clipboard.writeText === "function"
2745
- ) {
2746
- try {
2747
- await navigator.clipboard.writeText(handoff);
2748
- elements.copyButton.textContent = "Copiado";
2749
- return;
2750
- } catch {
2751
- // Selection fallback below.
2752
- }
2284
+ state.completionNotificationInFlight = false;
2753
2285
  }
2754
- elements.handoffCopy.focus();
2755
- elements.handoffCopy.select();
2756
- elements.copyButton.textContent = "Seleccionado para copiar";
2757
2286
  }
2758
2287
 
2759
2288
  document.addEventListener("DOMContentLoaded", initialize, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "2.9.2",
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.",
@@ -65,8 +65,8 @@
65
65
  "typescript": "^7.0.2",
66
66
  "vitest": "^2.1.9",
67
67
  "@packages/product-economics": "0.1.0",
68
- "@packages/types": "0.1.0",
69
- "@packages/product-research": "0.1.0"
68
+ "@packages/product-research": "0.1.0",
69
+ "@packages/types": "0.1.0"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public"