@productmaker/mcp 2.10.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/http.js CHANGED
@@ -28,11 +28,11 @@ var ApiClient = class {
28
28
  static {
29
29
  __name(this, "ApiClient");
30
30
  }
31
- baseUrl;
32
31
  apiKey;
32
+ baseUrl;
33
33
  constructor(baseUrl, apiKey) {
34
- this.baseUrl = baseUrl;
35
34
  this.apiKey = apiKey;
35
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
36
36
  }
37
37
  authHeaders() {
38
38
  return {
@@ -48,6 +48,10 @@ var ApiClient = class {
48
48
  fingerprint() {
49
49
  return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
50
50
  }
51
+ publicUrl(path3) {
52
+ const normalizedPath = path3.startsWith("/") ? path3 : `/${path3}`;
53
+ return new URL(`${this.baseUrl}${normalizedPath}`).toString();
54
+ }
51
55
  async getJson(path3) {
52
56
  const res = await fetch(`${this.baseUrl}${path3}`, {
53
57
  headers: this.authHeaders()
@@ -157,63 +161,76 @@ var ApiClient = class {
157
161
  };
158
162
 
159
163
  // src/index.ts
160
- var MCP_VERSION = "2.10.1";
164
+ var MCP_VERSION = "3.0.2";
161
165
 
162
166
  // src/meta-upload-app.ts
163
167
  import { readFileSync } from "fs";
164
168
  import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
165
- var META_UPLOAD_APP_RESOURCE_URI = "ui://meta-upload/v1.html";
166
169
  var MEDIA_UPLOAD_APP_RESOURCE_URI = "ui://media-upload/v1.html";
167
- var META_UPLOAD_APP_NAME = "ProductMaker media upload";
168
- var META_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
169
- var META_UPLOAD_APP_CSP = {
170
+ var MEDIA_UPLOAD_APP_NAME = "ProductMaker media upload";
171
+ var MEDIA_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
172
+ var MEDIA_UPLOAD_APP_CSP = {
170
173
  connectDomains: [
174
+ "https://api.productmaker.app",
171
175
  "https://productmaker.app",
172
176
  "https://www.productmaker.app",
173
177
  "https://*.r2.cloudflarestorage.com",
174
178
  "https://*.oaiusercontent.com"
175
179
  ]
176
180
  };
177
- function readMetaUploadAppHtml(htmlUrl = META_UPLOAD_APP_HTML_URL) {
181
+ function buildMediaUploadAppCsp(resolveEndpoint) {
182
+ const resolveOrigin = new URL(resolveEndpoint).origin;
183
+ return {
184
+ connectDomains: [
185
+ .../* @__PURE__ */ new Set([
186
+ ...MEDIA_UPLOAD_APP_CSP.connectDomains,
187
+ resolveOrigin
188
+ ])
189
+ ]
190
+ };
191
+ }
192
+ __name(buildMediaUploadAppCsp, "buildMediaUploadAppCsp");
193
+ function readMediaUploadAppHtml(htmlUrl = MEDIA_UPLOAD_APP_HTML_URL) {
178
194
  try {
179
195
  return readFileSync(htmlUrl, "utf8");
180
196
  } catch (error) {
181
197
  const message = error instanceof Error ? error.message : String(error);
182
- throw new Error(`Missing packaged Meta upload app HTML at ${htmlUrl.pathname}. Rebuild @productmaker/mcp so ${META_UPLOAD_APP_RESOURCE_URI} is bundled. Original error: ${message}`);
183
- }
184
- }
185
- __name(readMetaUploadAppHtml, "readMetaUploadAppHtml");
186
- function registerMetaUploadAppResource(server) {
187
- const html = readMetaUploadAppHtml();
188
- for (const resourceUri of [
189
- MEDIA_UPLOAD_APP_RESOURCE_URI,
190
- META_UPLOAD_APP_RESOURCE_URI
191
- ]) {
192
- registerAppResource(server, `${META_UPLOAD_APP_NAME} (${resourceUri})`, resourceUri, {
193
- title: "Upload media",
194
- mimeType: RESOURCE_MIME_TYPE,
195
- _meta: {
196
- ui: {
197
- csp: META_UPLOAD_APP_CSP
198
- }
198
+ throw new Error(`Missing packaged media upload app HTML at ${htmlUrl.pathname}. Rebuild @productmaker/mcp so ${MEDIA_UPLOAD_APP_RESOURCE_URI} is bundled. Original error: ${message}`);
199
+ }
200
+ }
201
+ __name(readMediaUploadAppHtml, "readMediaUploadAppHtml");
202
+ function renderMediaUploadAppHtml(resolveEndpoint) {
203
+ const encodedEndpoint = JSON.stringify(resolveEndpoint).slice(1, -1).replaceAll("<", "\\u003c");
204
+ return readMediaUploadAppHtml().replace("__PRODUCTMAKER_RESOLVE_ENDPOINT__", encodedEndpoint);
205
+ }
206
+ __name(renderMediaUploadAppHtml, "renderMediaUploadAppHtml");
207
+ function registerMediaUploadAppResource(server, resolveEndpoint) {
208
+ const html = renderMediaUploadAppHtml(resolveEndpoint);
209
+ const csp = buildMediaUploadAppCsp(resolveEndpoint);
210
+ registerAppResource(server, MEDIA_UPLOAD_APP_NAME, MEDIA_UPLOAD_APP_RESOURCE_URI, {
211
+ title: "Upload media",
212
+ mimeType: RESOURCE_MIME_TYPE,
213
+ _meta: {
214
+ ui: {
215
+ csp
199
216
  }
200
- }, async () => ({
201
- contents: [
202
- {
203
- uri: resourceUri,
204
- mimeType: RESOURCE_MIME_TYPE,
205
- text: html,
206
- _meta: {
207
- ui: {
208
- csp: META_UPLOAD_APP_CSP
209
- }
217
+ }
218
+ }, async () => ({
219
+ contents: [
220
+ {
221
+ uri: MEDIA_UPLOAD_APP_RESOURCE_URI,
222
+ mimeType: RESOURCE_MIME_TYPE,
223
+ text: html,
224
+ _meta: {
225
+ ui: {
226
+ csp
210
227
  }
211
228
  }
212
- ]
213
- }));
214
- }
229
+ }
230
+ ]
231
+ }));
215
232
  }
216
- __name(registerMetaUploadAppResource, "registerMetaUploadAppResource");
233
+ __name(registerMediaUploadAppResource, "registerMediaUploadAppResource");
217
234
 
218
235
  // src/tools/tasks.ts
219
236
  import { z as z47 } from "zod";
@@ -4911,46 +4928,47 @@ var DEFAULT_PRICING = {
4911
4928
  },
4912
4929
  // Grok Imagine I2V pricing by duration/resolution (grok-imagine/image-to-video).
4913
4930
  // Provider unified ALL grok-imagine-1.5 renders (silent multi-clip AND native-audio
4914
- // single-call) onto ONE per-second rate on 2026-06-22: $0.008/s @ 480p, $0.015/s @ 720p
4915
- // (was a split table where native audio billed ~10× the silent per-second tier).
4931
+ // single-call) onto ONE per-second rate on 2026-06-22 (then $0.008/s @ 480p,
4932
+ // $0.015/s @ 720p). Provider RAISED that unified rate on 2026-08-06 to
4933
+ // $0.012/s @ 480p and $0.0225/s @ 720p (+50%) — the rates below.
4916
4934
  // Flat duration keys below = perSecondRate × seconds; userPrice = GROK_USER_MARKUP × cost.
4917
4935
  //
4918
- // ⚠️ GROK BILLS AT GROK_USER_MARKUP (2.5×), NOT THE 2× HOUSE RATE — see the
4919
- // rationale on that constant above. Every other provider family stays at 2×.
4936
+ // Grok bills at GROK_USER_MARKUP, which is the 2× house rate again since
4937
+ // 2026-08-06 (the 2.5× exception was reverted — see that constant above).
4920
4938
  "kie:video-generation:grok-6s-480p": {
4921
- ourCostCents: 4.8,
4922
- userPriceCents: 12
4939
+ ourCostCents: 7.2,
4940
+ userPriceCents: 14.4
4923
4941
  },
4924
4942
  "kie:video-generation:grok-6s-720p": {
4925
- ourCostCents: 9,
4926
- userPriceCents: 22.5
4943
+ ourCostCents: 13.5,
4944
+ userPriceCents: 27
4927
4945
  },
4928
4946
  "kie:video-generation:grok-10s-480p": {
4929
- ourCostCents: 8,
4930
- userPriceCents: 20
4947
+ ourCostCents: 12,
4948
+ userPriceCents: 24
4931
4949
  },
4932
4950
  "kie:video-generation:grok-10s-720p": {
4933
- ourCostCents: 15,
4934
- userPriceCents: 37.5
4951
+ ourCostCents: 22.5,
4952
+ userPriceCents: 45
4935
4953
  },
4936
4954
  "kie:video-generation:grok-15s-480p": {
4937
- ourCostCents: 12,
4938
- userPriceCents: 30
4955
+ ourCostCents: 18,
4956
+ userPriceCents: 36
4939
4957
  },
4940
4958
  "kie:video-generation:grok-15s-720p": {
4941
- ourCostCents: 22.5,
4942
- userPriceCents: 56.25
4959
+ ourCostCents: 33.75,
4960
+ userPriceCents: 67.5
4943
4961
  },
4944
4962
  // Grok Imagine Video 1.5 PER-SECOND (rapid/standard multi-clip path; SAME unified
4945
- // $/s as the flat keys above: $0.008/s 480p, $0.015/s 720p). Billed units = perClipSeconds.
4963
+ // $/s as the flat keys above: $0.012/s 480p, $0.0225/s 720p). Billed units = perClipSeconds.
4946
4964
  // userPrice must stay flat_key ÷ 15 so a 15s render costs the same on either path.
4947
4965
  "kie:video-generation:grok15-persec-480p": {
4948
- ourCostCents: 0.8,
4949
- userPriceCents: 2
4966
+ ourCostCents: 1.2,
4967
+ userPriceCents: 2.4
4950
4968
  },
4951
4969
  "kie:video-generation:grok15-persec-720p": {
4952
- ourCostCents: 1.5,
4953
- userPriceCents: 3.75
4970
+ ourCostCents: 2.25,
4971
+ userPriceCents: 4.5
4954
4972
  },
4955
4973
  // Free-video subtitle burn: one final Whisper transcription per rendered second.
4956
4974
  // The worker uses this exact action with units=Math.ceil(finalAudioDuration).
@@ -5001,11 +5019,11 @@ var DEFAULT_PRICING = {
5001
5019
  // rate (same as kie: key above); ourCost = real Fal cost (we absorb the premium on rare failover).
5002
5020
  "fal:video-generation:grok-15s-480p": {
5003
5021
  ourCostCents: 121,
5004
- userPriceCents: 30
5022
+ userPriceCents: 36
5005
5023
  },
5006
5024
  "fal:video-generation:grok-15s-720p": {
5007
5025
  ourCostCents: 211,
5008
- userPriceCents: 56.25
5026
+ userPriceCents: 67.5
5009
5027
  },
5010
5028
  // Fal Grok Imagine 1.5 PER-SECOND failover pricing (rapid/STANDARD multi-clip path —
5011
5029
  // mirrors kie:video-generation:grok15-persec-* above, SAME $0.08/s (480p) / $0.14/s
@@ -5015,11 +5033,11 @@ var DEFAULT_PRICING = {
5015
5033
  // .superpowers/sdd/fal-grok-rapid-brief.md.
5016
5034
  "fal:video-generation:grok15-persec-480p": {
5017
5035
  ourCostCents: 8,
5018
- userPriceCents: 2
5036
+ userPriceCents: 2.4
5019
5037
  },
5020
5038
  "fal:video-generation:grok15-persec-720p": {
5021
5039
  ourCostCents: 14,
5022
- userPriceCents: 3.75
5040
+ userPriceCents: 4.5
5023
5041
  },
5024
5042
  "kie:video-generation:wan": {
5025
5043
  ourCostCents: 30,
@@ -5886,18 +5904,18 @@ var DEFAULT_PRICING = {
5886
5904
  },
5887
5905
  // ─── Video Regenerate pricing keys ────────────────────────────────────────
5888
5906
  // Billed by priceVideoRegenerateSection() below; keys kept for audit trail.
5889
- // multi_shot: one grok i2v clip per scene (~3s @ 480p: 0.8¢/s × 3s = 2.4¢ our / 6¢ user)
5890
- // single_call: one 15s native-audio grok-1.5 whole-video render (12¢ our / 30¢ user)
5907
+ // multi_shot: one grok i2v clip per scene (~3s @ 480p: 1.2¢/s × 3s = 3.6¢ our / 7.2¢ user)
5908
+ // single_call: one 15s native-audio grok-1.5 whole-video render (18¢ our / 36¢ user)
5891
5909
  // Both are grok renders, so both carry GROK_USER_MARKUP — and both match what the
5892
- // initial-render keys charge for the identical clip (grok15-persec-480p × 3s = 6¢;
5893
- // grok-15s-480p = 30¢).
5910
+ // initial-render keys charge for the identical clip (grok15-persec-480p × 3s = 7.2¢;
5911
+ // grok-15s-480p = 36¢).
5894
5912
  "kie:video-regen:multi_shot-per-scene": {
5895
- ourCostCents: 2.4,
5896
- userPriceCents: 6
5913
+ ourCostCents: 3.6,
5914
+ userPriceCents: 7.2
5897
5915
  },
5898
5916
  "kie:video-regen:single_call": {
5899
- ourCostCents: 12,
5900
- userPriceCents: 30
5917
+ ourCostCents: 18,
5918
+ userPriceCents: 36
5901
5919
  },
5902
5920
  // ─── Competitor Video Transcripts (OpenAI Whisper) ────────────────────────
5903
5921
  // Unit = one successfully transcribed video ad opening (~8s of audio).
@@ -8071,11 +8089,19 @@ var ServiceMediaAnalysisSchema = z43.object({
8071
8089
  });
8072
8090
 
8073
8091
  // ../../packages/types/src/productmaker-capability-registry.ts
8074
- var CHANNEL_EXPOSURE = {
8092
+ var SHARED_CHANNEL_EXPOSURE = {
8075
8093
  mcp: "direct",
8076
8094
  whatsapp: "adapted"
8077
8095
  };
8078
- function defineCapability(name, schemaHash, confirmation, routingDomain, effects, inputSlots, consumes, produces) {
8096
+ var MCP_ONLY_CHANNEL_EXPOSURE = {
8097
+ mcp: "direct",
8098
+ whatsapp: "unavailable"
8099
+ };
8100
+ var WHATSAPP_ONLY_CHANNEL_EXPOSURE = {
8101
+ mcp: "unavailable",
8102
+ whatsapp: "adapted"
8103
+ };
8104
+ function defineCapability(name, schemaHash, confirmation, routingDomain, effects, inputSlots, consumes, produces, channelExposure = SHARED_CHANNEL_EXPOSURE) {
8079
8105
  return {
8080
8106
  name,
8081
8107
  schemaHash,
@@ -8087,7 +8113,7 @@ function defineCapability(name, schemaHash, confirmation, routingDomain, effects
8087
8113
  consumes,
8088
8114
  produces
8089
8115
  },
8090
- channelExposure: CHANNEL_EXPOSURE
8116
+ channelExposure
8091
8117
  };
8092
8118
  }
8093
8119
  __name(defineCapability, "defineCapability");
@@ -8171,7 +8197,18 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8171
8197
  "meta_upload_session"
8172
8198
  ], [
8173
8199
  "staging_asset"
8174
- ]),
8200
+ ], WHATSAPP_ONLY_CHANNEL_EXPOSURE),
8201
+ defineCapability("check_media_upload", "daf202e0ff3a7a14425d164a7be6a81922b12233e85dbb898894e7c3ba6c5876", "none", "read_only", [
8202
+ "read"
8203
+ ], [
8204
+ slot("upload_session", true, [
8205
+ "sessionId"
8206
+ ], [])
8207
+ ], [
8208
+ "media_upload_session"
8209
+ ], [
8210
+ "uploaded_asset"
8211
+ ], MCP_ONLY_CHANNEL_EXPOSURE),
8175
8212
  defineCapability("cost_dropshipping_project", "746797b5400b170ba3a0704d432afff635794f0a7ae0b9da07b26d49e3b82242", "state_change", "business", [
8176
8213
  "state_change"
8177
8214
  ], [
@@ -8452,7 +8489,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8452
8489
  ], [
8453
8490
  "product_research_run"
8454
8491
  ]),
8455
- defineCapability("generate_free_video", "aa33aeb9a77976ba0841e3b11d1228038f63689ca9e635012f13e305a06ca37b", "credit_consuming", "creative", [
8492
+ defineCapability("generate_free_video", "01f5e412c4ae3a8e1d97ded33f5cf38c4505f02fcf301dad90c37655c8de783f", "credit_consuming", "creative", [
8456
8493
  "credit_consuming",
8457
8494
  "state_change"
8458
8495
  ], [
@@ -8793,7 +8830,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8793
8830
  ], [
8794
8831
  "tiktok_advertiser"
8795
8832
  ]),
8796
- defineCapability("publish_creatives_to_meta", "fe5ef259beaf113d74fd2d3f26f14fbbb3a7660be00ab44384441d2af837d731", "external_mutation", "publishing", [
8833
+ defineCapability("publish_creatives_to_meta", "b3a487134a1f5392d7a526348a03e952a317ae5d9ff01bae10bbe24ec5fe3fc8", "external_mutation", "publishing", [
8797
8834
  "external_mutation",
8798
8835
  "state_change"
8799
8836
  ], [
@@ -8962,7 +8999,30 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8962
8999
  "media_upload_request"
8963
9000
  ], [
8964
9001
  "meta_upload_session"
8965
- ]),
9002
+ ], WHATSAPP_ONLY_CHANNEL_EXPOSURE),
9003
+ defineCapability("request_media_upload", "d7ccfffe00f8563219fb3e036b07649ff59334e03fc52f6a2f3cb0823d4aa948", "none", "creative", [
9004
+ "state_change"
9005
+ ], [
9006
+ slot("media_kind", true, [
9007
+ "mediaKind"
9008
+ ], []),
9009
+ slot("asset_role", false, [
9010
+ "assetRole"
9011
+ ], []),
9012
+ slot("upload_purpose", false, [
9013
+ "uploadPurpose"
9014
+ ], []),
9015
+ slot("file_limit", false, [
9016
+ "maxFiles"
9017
+ ], []),
9018
+ slot("host_files", false, [
9019
+ "files"
9020
+ ], [])
9021
+ ], [
9022
+ "media_upload_request"
9023
+ ], [
9024
+ "media_upload_session"
9025
+ ], MCP_ONLY_CHANNEL_EXPOSURE),
8966
9026
  defineCapability("resolve_pending_action", "2fae350d14ba3fdab63914ca3b442472a002d97cffc85b6f207b1cc7e320ecdb", "credit_consuming", "creative", [
8967
9027
  "credit_consuming",
8968
9028
  "state_change"
@@ -9020,6 +9080,9 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9020
9080
  ])
9021
9081
  ]);
9022
9082
  var PRODUCTMAKER_CAPABILITY_NAMES = PRODUCTMAKER_CAPABILITY_REGISTRY.map((capability) => capability.name);
9083
+ var PRODUCTMAKER_MCP_CAPABILITY_REGISTRY = PRODUCTMAKER_CAPABILITY_REGISTRY.filter((capability) => capability.channelExposure.mcp === "direct");
9084
+ var PRODUCTMAKER_WHATSAPP_CAPABILITY_REGISTRY = PRODUCTMAKER_CAPABILITY_REGISTRY.filter((capability) => capability.channelExposure.whatsapp === "adapted");
9085
+ var PRODUCTMAKER_WHATSAPP_CAPABILITY_NAMES = PRODUCTMAKER_WHATSAPP_CAPABILITY_REGISTRY.map((capability) => capability.name);
9023
9086
 
9024
9087
  // ../../packages/types/src/productmaker-chat-capabilities.ts
9025
9088
  function toLegacyCapabilityCatalog(catalog) {
@@ -9030,7 +9093,8 @@ function toLegacyCapabilityCatalog(catalog) {
9030
9093
  }));
9031
9094
  }
9032
9095
  __name(toLegacyCapabilityCatalog, "toLegacyCapabilityCatalog");
9033
- var PRODUCTMAKER_MCP_CAPABILITIES = toLegacyCapabilityCatalog(PRODUCTMAKER_CAPABILITY_REGISTRY);
9096
+ var PRODUCTMAKER_MCP_CAPABILITIES = toLegacyCapabilityCatalog(PRODUCTMAKER_MCP_CAPABILITY_REGISTRY);
9097
+ var PRODUCTMAKER_MCP_CAPABILITY_NAMES = PRODUCTMAKER_MCP_CAPABILITY_REGISTRY.map((capability) => capability.name);
9034
9098
 
9035
9099
  // ../../packages/types/src/actor.ts
9036
9100
  import { z as z44 } from "zod";
@@ -11141,7 +11205,7 @@ var FREE_VIDEO_DURATIONS_BY_QUALITY = {
11141
11205
  ]
11142
11206
  };
11143
11207
  var FreeVideoInput = z48.object({
11144
- images: ImagesInput.optional().describe("Opcional. 1-5 fotos del producto. Om\xEDtelo para generar desde texto puro."),
11208
+ images: ImagesInput.optional().describe("Opcional. 1-5 fotos del producto. Si el usuario adjunt\xF3 o mencion\xF3 fotos locales y no existe un assetId reutilizable, llama primero request_media_upload con mediaKind=image y usa los assetId devueltos; no pidas rutas ni URLs. Om\xEDtelo para generar desde texto puro."),
11145
11209
  rawPrompt: z48.string().trim().min(1).max(2e3).describe("Prompt o guion exacto del video. Si activas subt\xEDtulos, este mismo texto se usa como guion de subt\xEDtulos."),
11146
11210
  quality: FreeVideoQualityEnum.describe('Calidad del video: "economical" = Econ\xF3mico o "high_detail" = Alto detalle.'),
11147
11211
  durationSeconds: z48.number().int().min(1).max(15).describe("Duraci\xF3n exacta. Econ\xF3mico permite 6, 8, 10 o 15s; Alto detalle permite 4, 8, 12 o 15s."),
@@ -11213,7 +11277,7 @@ var LandingInput = z48.object({
11213
11277
  subheadline: CreativeRefs.subheadline
11214
11278
  });
11215
11279
  function registerCreativeTools(server, api, { transport = "http" } = {}) {
11216
- server.tool("generate_free_video", 'Genera UN video libre con prompt/guion exacto, opcionalmente con imagen de referencia. Devuelve un taskId y procesa el job de forma as\xEDncrona.\n\nREQUERIDOS: rawPrompt, quality, durationSeconds y videoResolution.\n\nCALIDAD: "economical" = Econ\xF3mico y "high_detail" = Alto detalle. La duraci\xF3n debe pertenecer a la lista admitida por la calidad escogida; si el usuario pide otra, preg\xFAntale una duraci\xF3n v\xE1lida.\n\nSUBT\xCDTULOS: si addSubtitles=true, el rawPrompt completo se usa como texto exacto de subt\xEDtulos y debe tener m\xE1ximo 500 caracteres; no existe una pregunta adicional de transcripci\xF3n.\n\nPara videos normales con narrativa/\xE1ngulo usa generate_video_creative. Para tareas combinadas de producto usa create_product_task.', FreeVideoInput.shape, async (input) => {
11280
+ server.tool("generate_free_video", 'Genera UN video libre con prompt/guion exacto, opcionalmente con imagen de referencia. Si el usuario adjunta o menciona una imagen local, llama primero request_media_upload con mediaKind=image y usa el assetId devuelto; no pidas path ni URL. Devuelve un taskId y procesa el job de forma as\xEDncrona.\n\nREQUERIDOS: rawPrompt, quality, durationSeconds y videoResolution.\n\nCALIDAD: "economical" = Econ\xF3mico y "high_detail" = Alto detalle. La duraci\xF3n debe pertenecer a la lista admitida por la calidad escogida; si el usuario pide otra, preg\xFAntale una duraci\xF3n v\xE1lida.\n\nSUBT\xCDTULOS: si addSubtitles=true, el rawPrompt completo se usa como texto exacto de subt\xEDtulos y debe tener m\xE1ximo 500 caracteres; no existe una pregunta adicional de transcripci\xF3n.\n\nPara videos normales con narrativa/\xE1ngulo usa generate_video_creative. Para tareas combinadas de producto usa create_product_task.', FreeVideoInput.shape, async (input) => {
11217
11281
  try {
11218
11282
  const allowedDurations = FREE_VIDEO_DURATIONS_BY_QUALITY[input.quality];
11219
11283
  if (!allowedDurations.includes(input.durationSeconds)) {
@@ -11243,7 +11307,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
11243
11307
  throw translateError(e);
11244
11308
  }
11245
11309
  });
11246
- server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nCOMPETENCIA Y GAP REPLICATION: con videoPreset="auto" (default) y competitorScanEnabled=true (default), el sistema analiza anuncios de competencia. Si encuentra un winner de video coherente, ancla el primaryAngle y genera autom\xE1ticamente la narrativa `gap_replication` para copiar su estructura audiovisual. No la selecciones manualmente: es un resultado del an\xE1lisis. Si competitorScanEnabled=false, esta b\xFAsqueda se omite.\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
11310
+ server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio. Si el usuario adjunta o menciona fotos locales y a\xFAn no hay assetId, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nCOMPETENCIA Y GAP REPLICATION: con videoPreset="auto" (default) y competitorScanEnabled=true (default), el sistema analiza anuncios de competencia. Si encuentra un winner de video coherente, ancla el primaryAngle y genera autom\xE1ticamente la narrativa `gap_replication` para copiar su estructura audiovisual. No la selecciones manualmente: es un resultado del an\xE1lisis. Si competitorScanEnabled=false, esta b\xFAsqueda se omite.\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
11247
11311
  try {
11248
11312
  const resolved = resolveVideoPreset(input.videoPreset);
11249
11313
  const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
@@ -11268,7 +11332,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
11268
11332
  throw translateError(e);
11269
11333
  }
11270
11334
  });
11271
- server.tool("generate_image_creatives", 'Genera N variantes de imagen creativa (ads est\xE1ticos). Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), productTitle, primaryAngle, language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario:\n- price y offer (se renderizan en la imagen \u2014 nunca los asumas; si dice "t\xFA elige", om\xEDtelos).\n- compareAtPrice si hay descuento (ej. "$129.000" para tachar).\n- creativeArchetype y visualTreatment si quiere un look espec\xEDfico (performance vs premium DTC). Si no le importa, om\xEDtelos.\n\nNO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente.\n\nOTROS OPCIONALES: buyerPersona, nVariants (default 3), imageModel, imageStyle, styleMix, aspectRatio (default 4:5), generateAdCopy, pauseForAngleReview, features, brandColors, isCombo, creativeDirection.', ImageInput.shape, async (input) => {
11335
+ server.tool("generate_image_creatives", 'Genera N variantes de imagen creativa (ads est\xE1ticos). Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), productTitle, primaryAngle, language, country. Si el usuario adjunta o menciona fotos locales y a\xFAn no hay assetId, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario:\n- price y offer (se renderizan en la imagen \u2014 nunca los asumas; si dice "t\xFA elige", om\xEDtelos).\n- compareAtPrice si hay descuento (ej. "$129.000" para tachar).\n- creativeArchetype y visualTreatment si quiere un look espec\xEDfico (performance vs premium DTC). Si no le importa, om\xEDtelos.\n\nNO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente.\n\nOTROS OPCIONALES: buyerPersona, nVariants (default 3), imageModel, imageStyle, styleMix, aspectRatio (default 4:5), generateAdCopy, pauseForAngleReview, features, brandColors, isCombo, creativeDirection.', ImageInput.shape, async (input) => {
11272
11336
  try {
11273
11337
  const { projectId, ...rest } = input;
11274
11338
  return await submitCreative(api, {
@@ -11297,7 +11361,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
11297
11361
  throw translateError(e);
11298
11362
  }
11299
11363
  });
11300
- server.tool("generate_landing", 'Genera una landing page cl\xE1sica basada en im\xE1genes, lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para esos campos, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults): salesAngle, buyerPersona, isCombo, creativeDirection, landingStrategy.', LandingInput.shape, async (input) => {
11364
+ server.tool("generate_landing", 'Genera una landing page cl\xE1sica basada en im\xE1genes, lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), language, country. Si el usuario dice algo natural como \u201Ccrea una landing con este producto\u201D y adjunta o menciona fotos locales, llama primero `request_media_upload` con mediaKind=image y despu\xE9s usa los assetId en images; no pidas path ni URL.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para esos campos, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults): salesAngle, buyerPersona, isCombo, creativeDirection, landingStrategy.', LandingInput.shape, async (input) => {
11301
11365
  try {
11302
11366
  const { projectId, ...rest } = input;
11303
11367
  return await submitCreative(api, {
@@ -11853,12 +11917,6 @@ var GroupedMediaSource = z53.object({
11853
11917
  }).refine((value) => !(value.thumbnailUrl && value.thumbnailPath), {
11854
11918
  message: "media source needs at most one of `thumbnailUrl` or `thumbnailPath`"
11855
11919
  });
11856
- var GroupedStagedMediaSource = z53.object({
11857
- type: z53.literal("staged"),
11858
- /** Legacy source returned by check_meta_media_upload. */
11859
- stagingAssetId: z53.string().uuid(),
11860
- thumbnailAssetId: z53.string().uuid().optional()
11861
- });
11862
11920
  var GroupedUploadedAssetSource = z53.object({
11863
11921
  type: z53.literal("asset"),
11864
11922
  /** Returned by check_media_upload. ProductMaker resolves ownership server-side. */
@@ -11873,7 +11931,6 @@ var GroupedCreative = z53.object({
11873
11931
  source: z53.union([
11874
11932
  GroupedTaskSource,
11875
11933
  GroupedMediaSource,
11876
- GroupedStagedMediaSource,
11877
11934
  GroupedUploadedAssetSource
11878
11935
  ]),
11879
11936
  title: z53.string().max(120).optional(),
@@ -12105,7 +12162,7 @@ function registerPublishTools(server, api, opts = {}) {
12105
12162
  throw translateError(e);
12106
12163
  }
12107
12164
  });
12108
- 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 una laptop con MCP remoto llama `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL cuando el App est\xE1 disponible. `source.type="staged"` y `request_meta_media_upload` siguen aceptados solo por compatibilidad. Si la carga fue parcial, publica s\xF3lo los assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla, quedan durante el TTL para reintento. 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) => {
12165
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, publica s\xF3lo los assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla, quedan durante el TTL para reintento. 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) => {
12109
12166
  try {
12110
12167
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
12111
12168
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -12131,23 +12188,6 @@ function registerPublishTools(server, api, opts = {}) {
12131
12188
  });
12132
12189
  continue;
12133
12190
  }
12134
- if (creative.source.type === "staged") {
12135
- creatives.push({
12136
- type: creative.type,
12137
- source: {
12138
- type: "media",
12139
- mediaAssetId: creative.source.stagingAssetId,
12140
- ...creative.source.thumbnailAssetId ? {
12141
- thumbnailAssetId: creative.source.thumbnailAssetId
12142
- } : {}
12143
- },
12144
- title: creative.title,
12145
- description: creative.description,
12146
- cta: creative.cta,
12147
- creativeId
12148
- });
12149
- continue;
12150
- }
12151
12191
  if (creative.source.type === "asset") {
12152
12192
  creatives.push({
12153
12193
  type: creative.type,
@@ -12300,16 +12340,21 @@ __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
12300
12340
  // src/tools/meta-upload.ts
12301
12341
  import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
12302
12342
  import { z as z54 } from "zod";
12303
- var MetaUploadSessionInputBase = z54.object({
12343
+ var UploadSessionInputBase = z54.object({
12304
12344
  mediaKind: z54.enum([
12305
12345
  "image",
12306
12346
  "video",
12307
12347
  "mixed"
12308
- ]).default("mixed").describe("The kind of file the user will select in the upload page. Use mixed for a normal batch of image and video creatives."),
12348
+ ]).default("mixed").describe("The kind of file the user will select. Use image for product references and landing pages, video for a single video, and mixed only when the consumer supports both."),
12309
12349
  assetRole: z54.enum([
12310
12350
  "media",
12311
12351
  "thumbnail"
12312
12352
  ]).default("media").describe("Use media for the creative itself; thumbnail only for an image thumbnail."),
12353
+ uploadPurpose: z54.enum([
12354
+ "product_reference",
12355
+ "creative",
12356
+ "generic"
12357
+ ]).default("generic").describe("The user's purpose for these files. Use product_reference for a product photo used by tasks or landings, creative for media that will become an ad or creative, and generic only when neither applies."),
12313
12358
  maxFiles: z54.number().int().min(1).max(10).optional().describe("Optional batch limit: image accepts 1-5, video exactly 1, and mixed 1-10.")
12314
12359
  });
12315
12360
  var OpenAiFileReference = z54.object({
@@ -12318,18 +12363,18 @@ var OpenAiFileReference = z54.object({
12318
12363
  mime_type: z54.string().min(1).optional(),
12319
12364
  file_name: z54.string().min(1).optional()
12320
12365
  }).strict();
12321
- var MetaUploadSessionToolInputShape = {
12322
- ...MetaUploadSessionInputBase.shape,
12366
+ var UploadSessionToolInputShape = {
12367
+ ...UploadSessionInputBase.shape,
12323
12368
  files: z54.array(OpenAiFileReference).max(10).optional().describe("Optional OpenAI-hosted file references provided by ChatGPT. They are only metadata for the UI bridge and are never forwarded to ProductMaker.")
12324
12369
  };
12325
- var MetaUploadSessionToolInput = z54.object(MetaUploadSessionToolInputShape).superRefine((value, ctx) => {
12370
+ var UploadSessionToolInput = z54.object(UploadSessionToolInputShape).superRefine((value, ctx) => {
12326
12371
  if (value.assetRole === "thumbnail" && value.mediaKind !== "image") {
12327
12372
  ctx.addIssue({
12328
12373
  code: z54.ZodIssueCode.custom,
12329
12374
  path: [
12330
12375
  "mediaKind"
12331
12376
  ],
12332
- message: "Meta thumbnails must be images"
12377
+ message: "Thumbnails must be images"
12333
12378
  });
12334
12379
  }
12335
12380
  if (value.mediaKind === "video" && value.maxFiles !== void 0 && value.maxFiles !== 1) {
@@ -12351,7 +12396,7 @@ var MetaUploadSessionToolInput = z54.object(MetaUploadSessionToolInputShape).sup
12351
12396
  });
12352
12397
  }
12353
12398
  });
12354
- var MetaUploadSessionResponse2 = z54.object({
12399
+ var UploadSessionResponse = z54.object({
12355
12400
  sessionId: z54.string().uuid(),
12356
12401
  uploadPageUrl: z54.string().url(),
12357
12402
  mediaKind: z54.enum([
@@ -12366,23 +12411,8 @@ var MetaUploadSessionResponse2 = z54.object({
12366
12411
  maxFiles: z54.number().int().positive(),
12367
12412
  expiresAt: z54.string().datetime()
12368
12413
  });
12369
- var MetaUploadStatusInput = z54.object({
12370
- sessionId: z54.string().uuid().describe("The sessionId returned by request_meta_media_upload.")
12371
- });
12372
- var MetaUploadStatusResponse = z54.object({
12373
- sessionId: z54.string().uuid(),
12374
- status: z54.enum([
12375
- "pending",
12376
- "uploaded"
12377
- ]),
12378
- assets: z54.array(z54.object({
12379
- stagingAssetId: z54.string().uuid(),
12380
- mediaKind: z54.enum([
12381
- "image",
12382
- "video"
12383
- ])
12384
- })).max(10).default([]),
12385
- stagingAssetId: z54.string().uuid().optional()
12414
+ var UploadStatusInput = z54.object({
12415
+ sessionId: z54.string().uuid().describe("The sessionId returned by request_media_upload.")
12386
12416
  });
12387
12417
  var UniversalUploadStatusResponse = z54.discriminatedUnion("status", [
12388
12418
  z54.object({
@@ -12404,10 +12434,21 @@ var UniversalUploadStatusResponse = z54.discriminatedUnion("status", [
12404
12434
  })).min(1).max(10)
12405
12435
  })
12406
12436
  ]);
12407
- var UniversalUploadStructuredResponse = MetaUploadSessionResponse2.extend({
12437
+ var UniversalUploadStructuredResponse = UploadSessionResponse.extend({
12438
+ uploadPurpose: z54.enum([
12439
+ "product_reference",
12440
+ "creative",
12441
+ "generic"
12442
+ ]),
12408
12443
  checkToolName: z54.literal("check_media_upload"),
12409
- resolveEndpoint: z54.literal("/api/upload-media/resolve")
12444
+ resolveEndpoint: z54.string().url()
12410
12445
  });
12446
+ function contextualizeUploadPageUrl(uploadPageUrl, uploadPurpose) {
12447
+ const url = new URL(uploadPageUrl);
12448
+ url.searchParams.set("purpose", uploadPurpose);
12449
+ return url.toString();
12450
+ }
12451
+ __name(contextualizeUploadPageUrl, "contextualizeUploadPageUrl");
12411
12452
  function uploadSessionText(result) {
12412
12453
  const mediaLabel = result.mediaKind === "mixed" ? "archivos" : result.mediaKind === "video" ? "video" : "imagen";
12413
12454
  return {
@@ -12425,11 +12466,15 @@ function uploadSessionText(result) {
12425
12466
  };
12426
12467
  }
12427
12468
  __name(uploadSessionText, "uploadSessionText");
12428
- function registerMetaUploadTools(server, api) {
12469
+ function resolveEndpointForSession(api) {
12470
+ return api.publicUrl("/v1/uploaded-assets/sessions/resolve");
12471
+ }
12472
+ __name(resolveEndpointForSession, "resolveEndpointForSession");
12473
+ function registerMediaUploadTools(server, api) {
12429
12474
  registerAppTool(server, "request_media_upload", {
12430
12475
  title: "Subir archivos",
12431
- description: "Abre el selector seguro y reutilizable de ProductMaker para im\xE1genes o videos de la laptop. Es la ruta preferida para todas las herramientas compatibles: create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. En MCP remoto, si el usuario adjunta o menciona un archivo local, llama esta herramienta y no pidas una ruta local ni URL. Usa mediaKind=image para referencias de producto; no prometas usar un video donde el consumidor exige im\xE1genes. El usuario puede subir varios archivos y pulsa OK; el App contin\xFAa la petici\xF3n anterior usando \xFAnicamente asset IDs opacos y media kinds. Nunca muestres token, sessionId, URL temporal, bytes, base64 ni nombre privado del archivo. El enlace externo es fallback solo cuando el App inline no est\xE1 disponible o falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
12432
- inputSchema: MetaUploadSessionToolInputShape,
12476
+ description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. Tras OK, reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
12477
+ inputSchema: UploadSessionToolInputShape,
12433
12478
  outputSchema: UniversalUploadStructuredResponse.shape,
12434
12479
  _meta: {
12435
12480
  ui: {
@@ -12449,13 +12494,15 @@ function registerMetaUploadTools(server, api) {
12449
12494
  }
12450
12495
  }, async (rawInput) => {
12451
12496
  try {
12452
- const input = MetaUploadSessionToolInput.parse(rawInput);
12453
- const { files: _files, ...sessionInput } = input;
12454
- const session = MetaUploadSessionResponse2.parse(await api.postJson("/v1/uploaded-assets/sessions", sessionInput));
12497
+ const input = UploadSessionToolInput.parse(rawInput);
12498
+ const { files: _files, uploadPurpose, ...sessionInput } = input;
12499
+ const session = UploadSessionResponse.parse(await api.postJson("/v1/uploaded-assets/sessions", sessionInput));
12455
12500
  const result = UniversalUploadStructuredResponse.parse({
12456
12501
  ...session,
12502
+ uploadPageUrl: contextualizeUploadPageUrl(session.uploadPageUrl, uploadPurpose),
12503
+ uploadPurpose,
12457
12504
  checkToolName: "check_media_upload",
12458
- resolveEndpoint: "/api/upload-media/resolve"
12505
+ resolveEndpoint: resolveEndpointForSession(api)
12459
12506
  });
12460
12507
  return {
12461
12508
  structuredContent: result,
@@ -12467,69 +12514,20 @@ function registerMetaUploadTools(server, api) {
12467
12514
  });
12468
12515
  server.registerTool("check_media_upload", {
12469
12516
  description: "Comprueba la carga universal. Si status=uploaded, conserva assets[] para la petici\xF3n anterior y pasa cada assetId \xFAnicamente a un consumidor compatible. Un asset de video solo sirve donde se admite video; las referencias de producto, imagen y landing requieren mediaKind=image. No muestres IDs t\xE9cnicos como JSON al usuario.",
12470
- inputSchema: MetaUploadStatusInput.shape,
12517
+ inputSchema: UploadStatusInput.shape,
12471
12518
  _meta: {
12472
12519
  "openai/widgetAccessible": true
12473
12520
  }
12474
12521
  }, async (rawInput) => {
12475
12522
  try {
12476
- const input = MetaUploadStatusInput.parse(rawInput);
12523
+ const input = UploadStatusInput.parse(rawInput);
12477
12524
  return asText(UniversalUploadStatusResponse.parse(await api.getJson(`/v1/uploaded-assets/sessions/${encodeURIComponent(input.sessionId)}`)));
12478
12525
  } catch (error) {
12479
12526
  throw translateError(error);
12480
12527
  }
12481
12528
  });
12482
- registerAppTool(server, "request_meta_media_upload", {
12483
- title: "Solicitar carga segura de Meta",
12484
- description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Esta es la ruta DIRECTA para peticiones puntuales como \u2018sube esta imagen/video a Meta Ads\u2019 o \u2018publica este creativo\u2019; no conviertas esa intenci\xF3n en una generaci\xF3n completa de producto. No llames `guide_dropshipping_launch` para esta operaci\xF3n puntual. 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. El usuario pulsa OK cuando termina; el App inserta en el chat los asset_id verificados para continuar la petici\xF3n anterior. Luego llama `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres tokens, URLs temporales, `sessionId` ni un bloque JSON. Esta herramienta no publica ni consume cr\xE9ditos.",
12485
- inputSchema: MetaUploadSessionToolInputShape,
12486
- outputSchema: MetaUploadSessionResponse2.shape,
12487
- _meta: {
12488
- ui: {
12489
- resourceUri: META_UPLOAD_APP_RESOURCE_URI,
12490
- visibility: [
12491
- "model",
12492
- "app"
12493
- ]
12494
- },
12495
- "openai/outputTemplate": META_UPLOAD_APP_RESOURCE_URI,
12496
- "openai/widgetAccessible": true,
12497
- "openai/fileParams": [
12498
- "files"
12499
- ],
12500
- "openai/toolInvocation/invoking": "Preparando carga segura\u2026",
12501
- "openai/toolInvocation/invoked": "Carga segura lista"
12502
- }
12503
- }, async (rawInput) => {
12504
- try {
12505
- const input = MetaUploadSessionToolInput.parse(rawInput);
12506
- const { files: _files, ...sessionInput } = input;
12507
- const result = MetaUploadSessionResponse2.parse(await api.postJson("/v1/ads/meta-upload-sessions", sessionInput));
12508
- return {
12509
- structuredContent: result,
12510
- ...uploadSessionText(result)
12511
- };
12512
- } catch (error) {
12513
- throw translateError(error);
12514
- }
12515
- });
12516
- server.registerTool("check_meta_media_upload", {
12517
- 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.',
12518
- inputSchema: MetaUploadStatusInput.shape,
12519
- _meta: {
12520
- "openai/widgetAccessible": true
12521
- }
12522
- }, async (rawInput) => {
12523
- try {
12524
- const input = MetaUploadStatusInput.parse(rawInput);
12525
- const result = MetaUploadStatusResponse.parse(await api.getJson(`/v1/ads/meta-upload-sessions/${encodeURIComponent(input.sessionId)}`));
12526
- return asText(result);
12527
- } catch (error) {
12528
- throw translateError(error);
12529
- }
12530
- });
12531
12529
  }
12532
- __name(registerMetaUploadTools, "registerMetaUploadTools");
12530
+ __name(registerMediaUploadTools, "registerMediaUploadTools");
12533
12531
 
12534
12532
  // src/tools/campaigns.ts
12535
12533
  import { z as z55 } from "zod";
@@ -13572,13 +13570,13 @@ function registerDropshippingProjectTools(server, api) {
13572
13570
  throw translateError(error);
13573
13571
  }
13574
13572
  });
13575
- server.tool("guide_dropshipping_launch", "Gu\xEDa read-only de un recorrido COMPLETO de dropshipping, desde producto/costeo hasta creativos, tienda y publicidad. No uses este tool para subir o publicar un creativo puntual en Meta: llama directamente `request_meta_media_upload`, `list_meta_destinations` y `publish_creatives_to_meta`. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
13573
+ server.tool("guide_dropshipping_launch", "Gu\xEDa read-only de un recorrido COMPLETO de dropshipping, desde producto/costeo hasta creativos, tienda y publicidad. No uses este tool para subir o publicar un creativo puntual en Meta: llama directamente `request_media_upload`, `list_meta_destinations` y `publish_creatives_to_meta`. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
13576
13574
  try {
13577
13575
  if (!input.projectId) {
13578
13576
  return asText({
13579
13577
  mode: "choose",
13580
13578
  guided: "Usa start_dropshipping_project cuando el usuario quiera encontrar un producto y dejarlo listo para publicar.",
13581
- direct: "En modo directo usa la herramienta puntual. Para subir/publicar un creativo propio en Meta: request_meta_media_upload \u2192 list_meta_destinations \u2192 publish_creatives_to_meta. No crees un proyecto guiado.",
13579
+ direct: "En modo directo usa la herramienta puntual. Para subir/publicar un creativo propio en Meta: request_media_upload \u2192 list_meta_destinations \u2192 publish_creatives_to_meta. No crees un proyecto guiado.",
13582
13580
  confirmation: "Antes de acciones pagadas o externas, muestra el estimado y pide confirmaci\xF3n.",
13583
13581
  safety: "Las campa\xF1as Meta quedan PAUSED; el usuario las activa manualmente en Ads Manager."
13584
13582
  });
@@ -13594,7 +13592,10 @@ __name(registerDropshippingProjectTools, "registerDropshippingProjectTools");
13594
13592
  // src/guidance/productmaker-guidance.ts
13595
13593
  var START_DROPSHIPPING_PROMPT_NAME = "start_dropshipping";
13596
13594
  var PRODUCTMAKER_SERVER_INSTRUCTIONS = [
13597
- "Understand the user's intent before choosing a ProductMaker tool.",
13595
+ "When a ProductMaker request needs an image or video the user attached, pasted, or has locally and no reusable ProductMaker assetId is already available, call request_media_upload immediately before asking for any missing business inputs. Do this even when the user says not to generate or publish yet: uploading only stages the file and does not generate, publish, or consume credits, unless the user explicitly says not to upload, share, or store the file. Treat natural requests such as 'create a landing with this product', 'make creatives with this photo', or 'upload this video to Meta' as upload intent; never require the user to name the uploader.",
13596
+ "For requests that do not need a new media upload, understand the user's intent before choosing a ProductMaker tool.",
13597
+ "For remote MCP hosts, never ask for a local path, CDN URL, bytes, or base64. Pass supported host file references to request_media_upload; otherwise let its inline App open the file picker. The external upload page is fallback only.",
13598
+ "After the upload App sends its OK continuation, resume the original request with the verified assetId values, do not ask for another upload, and ask only for remaining business inputs in chat. The host-inserted continuation may contain opaque asset IDs; do not repeat them in narrative prose or expose session IDs, tokens, private URLs, file names, or JSON handoff payloads.",
13598
13599
  "Keep focused requests direct: creatives, landing pages, Shopify publication, Meta publication, and status checks do not require a DropshippingProject.",
13599
13600
  "Use the guided journey only for an explicit end-to-end launch.",
13600
13601
  "In guided mode, create a project only after a product is chosen, then call guide_dropshipping_launch after every mutation and follow its nextAction.",
@@ -13659,7 +13660,7 @@ function createServer(opts) {
13659
13660
  });
13660
13661
  const api = new ApiClient(apiBaseUrl, apiKey);
13661
13662
  registerProductMakerPrompts(server);
13662
- registerMetaUploadAppResource(server);
13663
+ registerMediaUploadAppResource(server, api.publicUrl("/v1/uploaded-assets/sessions/resolve"));
13663
13664
  registerProductCostingTools(server);
13664
13665
  registerProductResearchTools(server, api);
13665
13666
  registerDropshippingProjectTools(server, api);
@@ -13677,7 +13678,7 @@ function createServer(opts) {
13677
13678
  registerPublishTools(server, api, {
13678
13679
  transport
13679
13680
  });
13680
- registerMetaUploadTools(server, api);
13681
+ registerMediaUploadTools(server, api);
13681
13682
  registerCampaignTools(server, api);
13682
13683
  return server;
13683
13684
  }
@@ -13694,6 +13695,9 @@ function assertSafePublicBaseUrl(envName, url) {
13694
13695
  if (u.protocol !== "https:") {
13695
13696
  throw new Error(`${envName} must use https (got ${u.protocol})`);
13696
13697
  }
13698
+ if (u.username || u.password || u.search || u.hash) {
13699
+ throw new Error(`${envName} must not contain credentials, query parameters, or fragments`);
13700
+ }
13697
13701
  if (isPrivateHost(u.hostname)) {
13698
13702
  throw new Error(`${envName} must not point to a private/loopback host: ${u.hostname}`);
13699
13703
  }