@productmaker/mcp 3.0.0 → 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,7 +161,7 @@ var ApiClient = class {
157
161
  };
158
162
 
159
163
  // src/index.ts
160
- var MCP_VERSION = "3.0.0";
164
+ var MCP_VERSION = "3.0.2";
161
165
 
162
166
  // src/meta-upload-app.ts
163
167
  import { readFileSync } from "fs";
@@ -167,12 +171,25 @@ var MEDIA_UPLOAD_APP_NAME = "ProductMaker media upload";
167
171
  var MEDIA_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
168
172
  var MEDIA_UPLOAD_APP_CSP = {
169
173
  connectDomains: [
174
+ "https://api.productmaker.app",
170
175
  "https://productmaker.app",
171
176
  "https://www.productmaker.app",
172
177
  "https://*.r2.cloudflarestorage.com",
173
178
  "https://*.oaiusercontent.com"
174
179
  ]
175
180
  };
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");
176
193
  function readMediaUploadAppHtml(htmlUrl = MEDIA_UPLOAD_APP_HTML_URL) {
177
194
  try {
178
195
  return readFileSync(htmlUrl, "utf8");
@@ -182,14 +199,20 @@ function readMediaUploadAppHtml(htmlUrl = MEDIA_UPLOAD_APP_HTML_URL) {
182
199
  }
183
200
  }
184
201
  __name(readMediaUploadAppHtml, "readMediaUploadAppHtml");
185
- function registerMediaUploadAppResource(server) {
186
- const html = 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);
187
210
  registerAppResource(server, MEDIA_UPLOAD_APP_NAME, MEDIA_UPLOAD_APP_RESOURCE_URI, {
188
211
  title: "Upload media",
189
212
  mimeType: RESOURCE_MIME_TYPE,
190
213
  _meta: {
191
214
  ui: {
192
- csp: MEDIA_UPLOAD_APP_CSP
215
+ csp
193
216
  }
194
217
  }
195
218
  }, async () => ({
@@ -200,7 +223,7 @@ function registerMediaUploadAppResource(server) {
200
223
  text: html,
201
224
  _meta: {
202
225
  ui: {
203
- csp: MEDIA_UPLOAD_APP_CSP
226
+ csp
204
227
  }
205
228
  }
206
229
  }
@@ -8977,7 +9000,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8977
9000
  ], [
8978
9001
  "meta_upload_session"
8979
9002
  ], WHATSAPP_ONLY_CHANNEL_EXPOSURE),
8980
- defineCapability("request_media_upload", "794e9b279b49d0e818423fa4ae9f1a4a52689620abc3520380d7e86a3630380f", "none", "creative", [
9003
+ defineCapability("request_media_upload", "d7ccfffe00f8563219fb3e036b07649ff59334e03fc52f6a2f3cb0823d4aa948", "none", "creative", [
8981
9004
  "state_change"
8982
9005
  ], [
8983
9006
  slot("media_kind", true, [
@@ -8986,6 +9009,9 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8986
9009
  slot("asset_role", false, [
8987
9010
  "assetRole"
8988
9011
  ], []),
9012
+ slot("upload_purpose", false, [
9013
+ "uploadPurpose"
9014
+ ], []),
8989
9015
  slot("file_limit", false, [
8990
9016
  "maxFiles"
8991
9017
  ], []),
@@ -12324,6 +12350,11 @@ var UploadSessionInputBase = z54.object({
12324
12350
  "media",
12325
12351
  "thumbnail"
12326
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."),
12327
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.")
12328
12359
  });
12329
12360
  var OpenAiFileReference = z54.object({
@@ -12404,9 +12435,20 @@ var UniversalUploadStatusResponse = z54.discriminatedUnion("status", [
12404
12435
  })
12405
12436
  ]);
12406
12437
  var UniversalUploadStructuredResponse = UploadSessionResponse.extend({
12438
+ uploadPurpose: z54.enum([
12439
+ "product_reference",
12440
+ "creative",
12441
+ "generic"
12442
+ ]),
12407
12443
  checkToolName: z54.literal("check_media_upload"),
12408
- resolveEndpoint: z54.literal("/api/upload-media/resolve")
12444
+ resolveEndpoint: z54.string().url()
12409
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");
12410
12452
  function uploadSessionText(result) {
12411
12453
  const mediaLabel = result.mediaKind === "mixed" ? "archivos" : result.mediaKind === "video" ? "video" : "imagen";
12412
12454
  return {
@@ -12424,10 +12466,14 @@ function uploadSessionText(result) {
12424
12466
  };
12425
12467
  }
12426
12468
  __name(uploadSessionText, "uploadSessionText");
12469
+ function resolveEndpointForSession(api) {
12470
+ return api.publicUrl("/v1/uploaded-assets/sessions/resolve");
12471
+ }
12472
+ __name(resolveEndpointForSession, "resolveEndpointForSession");
12427
12473
  function registerMediaUploadTools(server, api) {
12428
12474
  registerAppTool(server, "request_media_upload", {
12429
12475
  title: "Subir archivos",
12430
- description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente 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; 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 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.",
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.",
12431
12477
  inputSchema: UploadSessionToolInputShape,
12432
12478
  outputSchema: UniversalUploadStructuredResponse.shape,
12433
12479
  _meta: {
@@ -12449,12 +12495,14 @@ function registerMediaUploadTools(server, api) {
12449
12495
  }, async (rawInput) => {
12450
12496
  try {
12451
12497
  const input = UploadSessionToolInput.parse(rawInput);
12452
- const { files: _files, ...sessionInput } = input;
12498
+ const { files: _files, uploadPurpose, ...sessionInput } = input;
12453
12499
  const session = UploadSessionResponse.parse(await api.postJson("/v1/uploaded-assets/sessions", sessionInput));
12454
12500
  const result = UniversalUploadStructuredResponse.parse({
12455
12501
  ...session,
12502
+ uploadPageUrl: contextualizeUploadPageUrl(session.uploadPageUrl, uploadPurpose),
12503
+ uploadPurpose,
12456
12504
  checkToolName: "check_media_upload",
12457
- resolveEndpoint: "/api/upload-media/resolve"
12505
+ resolveEndpoint: resolveEndpointForSession(api)
12458
12506
  });
12459
12507
  return {
12460
12508
  structuredContent: result,
@@ -13544,8 +13592,8 @@ __name(registerDropshippingProjectTools, "registerDropshippingProjectTools");
13544
13592
  // src/guidance/productmaker-guidance.ts
13545
13593
  var START_DROPSHIPPING_PROMPT_NAME = "start_dropshipping";
13546
13594
  var PRODUCTMAKER_SERVER_INSTRUCTIONS = [
13547
- "Understand the user's intent before choosing a ProductMaker tool.",
13548
- "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. 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.",
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.",
13549
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.",
13550
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.",
13551
13599
  "Keep focused requests direct: creatives, landing pages, Shopify publication, Meta publication, and status checks do not require a DropshippingProject.",
@@ -13612,7 +13660,7 @@ function createServer(opts) {
13612
13660
  });
13613
13661
  const api = new ApiClient(apiBaseUrl, apiKey);
13614
13662
  registerProductMakerPrompts(server);
13615
- registerMediaUploadAppResource(server);
13663
+ registerMediaUploadAppResource(server, api.publicUrl("/v1/uploaded-assets/sessions/resolve"));
13616
13664
  registerProductCostingTools(server);
13617
13665
  registerProductResearchTools(server, api);
13618
13666
  registerDropshippingProjectTools(server, api);
@@ -13647,6 +13695,9 @@ function assertSafePublicBaseUrl(envName, url) {
13647
13695
  if (u.protocol !== "https:") {
13648
13696
  throw new Error(`${envName} must use https (got ${u.protocol})`);
13649
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
+ }
13650
13701
  if (isPrivateHost(u.hostname)) {
13651
13702
  throw new Error(`${envName} must not point to a private/loopback host: ${u.hostname}`);
13652
13703
  }
package/dist/stdio.js CHANGED
@@ -25,11 +25,11 @@ var ApiClient = class {
25
25
  static {
26
26
  __name(this, "ApiClient");
27
27
  }
28
- baseUrl;
29
28
  apiKey;
29
+ baseUrl;
30
30
  constructor(baseUrl, apiKey) {
31
- this.baseUrl = baseUrl;
32
31
  this.apiKey = apiKey;
32
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
33
33
  }
34
34
  authHeaders() {
35
35
  return {
@@ -45,6 +45,10 @@ var ApiClient = class {
45
45
  fingerprint() {
46
46
  return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
47
47
  }
48
+ publicUrl(path3) {
49
+ const normalizedPath = path3.startsWith("/") ? path3 : `/${path3}`;
50
+ return new URL(`${this.baseUrl}${normalizedPath}`).toString();
51
+ }
48
52
  async getJson(path3) {
49
53
  const res = await fetch(`${this.baseUrl}${path3}`, {
50
54
  headers: this.authHeaders()
@@ -154,7 +158,7 @@ var ApiClient = class {
154
158
  };
155
159
 
156
160
  // src/index.ts
157
- var MCP_VERSION = "3.0.0";
161
+ var MCP_VERSION = "3.0.2";
158
162
 
159
163
  // src/meta-upload-app.ts
160
164
  import { readFileSync } from "fs";
@@ -164,12 +168,25 @@ var MEDIA_UPLOAD_APP_NAME = "ProductMaker media upload";
164
168
  var MEDIA_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
165
169
  var MEDIA_UPLOAD_APP_CSP = {
166
170
  connectDomains: [
171
+ "https://api.productmaker.app",
167
172
  "https://productmaker.app",
168
173
  "https://www.productmaker.app",
169
174
  "https://*.r2.cloudflarestorage.com",
170
175
  "https://*.oaiusercontent.com"
171
176
  ]
172
177
  };
178
+ function buildMediaUploadAppCsp(resolveEndpoint) {
179
+ const resolveOrigin = new URL(resolveEndpoint).origin;
180
+ return {
181
+ connectDomains: [
182
+ .../* @__PURE__ */ new Set([
183
+ ...MEDIA_UPLOAD_APP_CSP.connectDomains,
184
+ resolveOrigin
185
+ ])
186
+ ]
187
+ };
188
+ }
189
+ __name(buildMediaUploadAppCsp, "buildMediaUploadAppCsp");
173
190
  function readMediaUploadAppHtml(htmlUrl = MEDIA_UPLOAD_APP_HTML_URL) {
174
191
  try {
175
192
  return readFileSync(htmlUrl, "utf8");
@@ -179,14 +196,20 @@ function readMediaUploadAppHtml(htmlUrl = MEDIA_UPLOAD_APP_HTML_URL) {
179
196
  }
180
197
  }
181
198
  __name(readMediaUploadAppHtml, "readMediaUploadAppHtml");
182
- function registerMediaUploadAppResource(server) {
183
- const html = readMediaUploadAppHtml();
199
+ function renderMediaUploadAppHtml(resolveEndpoint) {
200
+ const encodedEndpoint = JSON.stringify(resolveEndpoint).slice(1, -1).replaceAll("<", "\\u003c");
201
+ return readMediaUploadAppHtml().replace("__PRODUCTMAKER_RESOLVE_ENDPOINT__", encodedEndpoint);
202
+ }
203
+ __name(renderMediaUploadAppHtml, "renderMediaUploadAppHtml");
204
+ function registerMediaUploadAppResource(server, resolveEndpoint) {
205
+ const html = renderMediaUploadAppHtml(resolveEndpoint);
206
+ const csp = buildMediaUploadAppCsp(resolveEndpoint);
184
207
  registerAppResource(server, MEDIA_UPLOAD_APP_NAME, MEDIA_UPLOAD_APP_RESOURCE_URI, {
185
208
  title: "Upload media",
186
209
  mimeType: RESOURCE_MIME_TYPE,
187
210
  _meta: {
188
211
  ui: {
189
- csp: MEDIA_UPLOAD_APP_CSP
212
+ csp
190
213
  }
191
214
  }
192
215
  }, async () => ({
@@ -197,7 +220,7 @@ function registerMediaUploadAppResource(server) {
197
220
  text: html,
198
221
  _meta: {
199
222
  ui: {
200
- csp: MEDIA_UPLOAD_APP_CSP
223
+ csp
201
224
  }
202
225
  }
203
226
  }
@@ -8974,7 +8997,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8974
8997
  ], [
8975
8998
  "meta_upload_session"
8976
8999
  ], WHATSAPP_ONLY_CHANNEL_EXPOSURE),
8977
- defineCapability("request_media_upload", "794e9b279b49d0e818423fa4ae9f1a4a52689620abc3520380d7e86a3630380f", "none", "creative", [
9000
+ defineCapability("request_media_upload", "d7ccfffe00f8563219fb3e036b07649ff59334e03fc52f6a2f3cb0823d4aa948", "none", "creative", [
8978
9001
  "state_change"
8979
9002
  ], [
8980
9003
  slot("media_kind", true, [
@@ -8983,6 +9006,9 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
8983
9006
  slot("asset_role", false, [
8984
9007
  "assetRole"
8985
9008
  ], []),
9009
+ slot("upload_purpose", false, [
9010
+ "uploadPurpose"
9011
+ ], []),
8986
9012
  slot("file_limit", false, [
8987
9013
  "maxFiles"
8988
9014
  ], []),
@@ -12280,6 +12306,11 @@ var UploadSessionInputBase = z54.object({
12280
12306
  "media",
12281
12307
  "thumbnail"
12282
12308
  ]).default("media").describe("Use media for the creative itself; thumbnail only for an image thumbnail."),
12309
+ uploadPurpose: z54.enum([
12310
+ "product_reference",
12311
+ "creative",
12312
+ "generic"
12313
+ ]).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."),
12283
12314
  maxFiles: z54.number().int().min(1).max(10).optional().describe("Optional batch limit: image accepts 1-5, video exactly 1, and mixed 1-10.")
12284
12315
  });
12285
12316
  var OpenAiFileReference = z54.object({
@@ -12360,9 +12391,20 @@ var UniversalUploadStatusResponse = z54.discriminatedUnion("status", [
12360
12391
  })
12361
12392
  ]);
12362
12393
  var UniversalUploadStructuredResponse = UploadSessionResponse.extend({
12394
+ uploadPurpose: z54.enum([
12395
+ "product_reference",
12396
+ "creative",
12397
+ "generic"
12398
+ ]),
12363
12399
  checkToolName: z54.literal("check_media_upload"),
12364
- resolveEndpoint: z54.literal("/api/upload-media/resolve")
12400
+ resolveEndpoint: z54.string().url()
12365
12401
  });
12402
+ function contextualizeUploadPageUrl(uploadPageUrl, uploadPurpose) {
12403
+ const url = new URL(uploadPageUrl);
12404
+ url.searchParams.set("purpose", uploadPurpose);
12405
+ return url.toString();
12406
+ }
12407
+ __name(contextualizeUploadPageUrl, "contextualizeUploadPageUrl");
12366
12408
  function uploadSessionText(result) {
12367
12409
  const mediaLabel = result.mediaKind === "mixed" ? "archivos" : result.mediaKind === "video" ? "video" : "imagen";
12368
12410
  return {
@@ -12380,10 +12422,14 @@ function uploadSessionText(result) {
12380
12422
  };
12381
12423
  }
12382
12424
  __name(uploadSessionText, "uploadSessionText");
12425
+ function resolveEndpointForSession(api) {
12426
+ return api.publicUrl("/v1/uploaded-assets/sessions/resolve");
12427
+ }
12428
+ __name(resolveEndpointForSession, "resolveEndpointForSession");
12383
12429
  function registerMediaUploadTools(server, api) {
12384
12430
  registerAppTool(server, "request_media_upload", {
12385
12431
  title: "Subir archivos",
12386
- description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente 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; 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 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.",
12432
+ 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.",
12387
12433
  inputSchema: UploadSessionToolInputShape,
12388
12434
  outputSchema: UniversalUploadStructuredResponse.shape,
12389
12435
  _meta: {
@@ -12405,12 +12451,14 @@ function registerMediaUploadTools(server, api) {
12405
12451
  }, async (rawInput) => {
12406
12452
  try {
12407
12453
  const input = UploadSessionToolInput.parse(rawInput);
12408
- const { files: _files, ...sessionInput } = input;
12454
+ const { files: _files, uploadPurpose, ...sessionInput } = input;
12409
12455
  const session = UploadSessionResponse.parse(await api.postJson("/v1/uploaded-assets/sessions", sessionInput));
12410
12456
  const result = UniversalUploadStructuredResponse.parse({
12411
12457
  ...session,
12458
+ uploadPageUrl: contextualizeUploadPageUrl(session.uploadPageUrl, uploadPurpose),
12459
+ uploadPurpose,
12412
12460
  checkToolName: "check_media_upload",
12413
- resolveEndpoint: "/api/upload-media/resolve"
12461
+ resolveEndpoint: resolveEndpointForSession(api)
12414
12462
  });
12415
12463
  return {
12416
12464
  structuredContent: result,
@@ -13500,8 +13548,8 @@ __name(registerDropshippingProjectTools, "registerDropshippingProjectTools");
13500
13548
  // src/guidance/productmaker-guidance.ts
13501
13549
  var START_DROPSHIPPING_PROMPT_NAME = "start_dropshipping";
13502
13550
  var PRODUCTMAKER_SERVER_INSTRUCTIONS = [
13503
- "Understand the user's intent before choosing a ProductMaker tool.",
13504
- "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. 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.",
13551
+ "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.",
13552
+ "For requests that do not need a new media upload, understand the user's intent before choosing a ProductMaker tool.",
13505
13553
  "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.",
13506
13554
  "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.",
13507
13555
  "Keep focused requests direct: creatives, landing pages, Shopify publication, Meta publication, and status checks do not require a DropshippingProject.",
@@ -13568,7 +13616,7 @@ function createServer(opts) {
13568
13616
  });
13569
13617
  const api = new ApiClient(apiBaseUrl, apiKey);
13570
13618
  registerProductMakerPrompts(server);
13571
- registerMediaUploadAppResource(server);
13619
+ registerMediaUploadAppResource(server, api.publicUrl("/v1/uploaded-assets/sessions/resolve"));
13572
13620
  registerProductCostingTools(server);
13573
13621
  registerProductResearchTools(server, api);
13574
13622
  registerDropshippingProjectTools(server, api);
@@ -13603,6 +13651,9 @@ function assertSafePublicBaseUrl(envName, url) {
13603
13651
  if (u.protocol !== "https:") {
13604
13652
  throw new Error(`${envName} must use https (got ${u.protocol})`);
13605
13653
  }
13654
+ if (u.username || u.password || u.search || u.hash) {
13655
+ throw new Error(`${envName} must not contain credentials, query parameters, or fragments`);
13656
+ }
13606
13657
  if (isPrivateHost(u.hostname)) {
13607
13658
  throw new Error(`${envName} must not point to a private/loopback host: ${u.hostname}`);
13608
13659
  }
@@ -147,6 +147,13 @@
147
147
  padding: 18px;
148
148
  }
149
149
 
150
+ .upload-prompt {
151
+ margin: 0 0 12px;
152
+ color: var(--pm-text);
153
+ font-size: 15px;
154
+ font-weight: 700;
155
+ }
156
+
150
157
  .media-grid {
151
158
  display: grid;
152
159
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
@@ -435,6 +442,9 @@
435
442
  <main class="app" data-testid="meta-upload-root">
436
443
  <article class="card">
437
444
  <section class="section" aria-label="Upload media">
445
+ <p class="upload-prompt" data-testid="meta-upload-prompt">
446
+ Sube tus archivos
447
+ </p>
438
448
  <div class="media-grid">
439
449
  <div
440
450
  class="file-list"
@@ -535,7 +545,6 @@
535
545
  const MAX_FILES = 10;
536
546
  const MAX_FILE_SIZE_BYTES = 350 * 1024 * 1024;
537
547
  const MAX_VIDEO_DURATION_SECONDS = 180;
538
- const MIN_DIMENSION = 600;
539
548
  const IMAGE_COMPRESSION_THRESHOLD_BYTES = 1.8 * 1024 * 1024;
540
549
  const IMAGE_COMPRESSION_MAX_BYTES = 1.8 * 1024 * 1024;
541
550
  const IMAGE_MAX_DIMENSION = 2048;
@@ -543,6 +552,7 @@
543
552
  const TOOL_RESULT_TEXT_LIMIT = 100000;
544
553
  const RPC_TIMEOUT_MS = 15000;
545
554
  const APP_PROTOCOL_VERSION = "2026-01-26";
555
+ const CONFIGURED_RESOLVE_ENDPOINT = "__PRODUCTMAKER_RESOLVE_ENDPOINT__";
546
556
 
547
557
  const elements = {
548
558
  root: document.querySelector('[data-testid="meta-upload-root"]'),
@@ -568,6 +578,9 @@
568
578
  completeButton: document.querySelector(
569
579
  '[data-testid="meta-upload-complete-btn"]',
570
580
  ),
581
+ uploadPrompt: document.querySelector(
582
+ '[data-testid="meta-upload-prompt"]',
583
+ ),
571
584
  fileList: document.querySelector(
572
585
  '[data-testid="meta-upload-file-list"]',
573
586
  ),
@@ -1156,6 +1169,7 @@
1156
1169
  }
1157
1170
  }
1158
1171
  state.session = session;
1172
+ elements.uploadPrompt.textContent = uploadPromptForSession(session);
1159
1173
  elements.fallbackLink.href = session.uploadPageUrl;
1160
1174
  if (sessionExpired()) {
1161
1175
  requireFallback(
@@ -1178,6 +1192,7 @@
1178
1192
  const uploadPageUrl = boundedString(candidate.uploadPageUrl, 2000);
1179
1193
  const mediaKind = candidate.mediaKind;
1180
1194
  const assetRole = candidate.assetRole;
1195
+ const uploadPurpose = candidate.uploadPurpose;
1181
1196
  const expiresAt = boundedString(candidate.expiresAt, 100);
1182
1197
  const checkToolName = "check_media_upload";
1183
1198
  const resolveEndpoint =
@@ -1189,7 +1204,10 @@
1189
1204
  if (!Number.isInteger(candidate.maxFiles) || !expiresAt) return null;
1190
1205
  try {
1191
1206
  const parsedUrl = new URL(uploadPageUrl);
1192
- if (!safeUploadPageUrl(parsedUrl)) return null;
1207
+ const parsedResolveUrl = new URL(resolveEndpoint, parsedUrl.origin);
1208
+ if (!safeResolveEndpointUrl(parsedResolveUrl, parsedUrl))
1209
+ return null;
1210
+ if (!safeUploadPageUrl(parsedUrl, parsedResolveUrl)) return null;
1193
1211
  } catch {
1194
1212
  return null;
1195
1213
  }
@@ -1198,6 +1216,11 @@
1198
1216
  uploadPageUrl,
1199
1217
  mediaKind,
1200
1218
  assetRole,
1219
+ uploadPurpose:
1220
+ uploadPurpose === "product_reference" ||
1221
+ uploadPurpose === "creative"
1222
+ ? uploadPurpose
1223
+ : "generic",
1201
1224
  maxFiles: candidate.maxFiles,
1202
1225
  expiresAt,
1203
1226
  checkToolName,
@@ -1205,6 +1228,26 @@
1205
1228
  };
1206
1229
  }
1207
1230
 
1231
+ function uploadPromptForSession(session) {
1232
+ if (session.uploadPurpose === "product_reference") {
1233
+ return session.mediaKind === "image"
1234
+ ? "Sube la foto del producto"
1235
+ : session.mediaKind === "video"
1236
+ ? "Sube el video del producto"
1237
+ : "Sube los archivos del producto";
1238
+ }
1239
+ if (session.uploadPurpose === "creative") {
1240
+ return session.mediaKind === "video"
1241
+ ? "Sube tu video creativo"
1242
+ : "Sube tus creativos";
1243
+ }
1244
+ return session.mediaKind === "image"
1245
+ ? "Sube tus imágenes"
1246
+ : session.mediaKind === "video"
1247
+ ? "Sube tu video"
1248
+ : "Sube tus archivos";
1249
+ }
1250
+
1208
1251
  function mergeToolInput(input) {
1209
1252
  state.toolInput = { ...state.toolInput, ...input };
1210
1253
  void consumePendingBridgeFiles();
@@ -1630,15 +1673,6 @@
1630
1673
  mediaKind === "image"
1631
1674
  ? await inspectImage(file)
1632
1675
  : await inspectVideo(file);
1633
- if (
1634
- inspection.width < MIN_DIMENSION ||
1635
- inspection.height < MIN_DIMENSION
1636
- ) {
1637
- throw validationError(
1638
- "low_resolution",
1639
- "La resolución mínima es 600 × 600 px.",
1640
- );
1641
- }
1642
1676
  if (
1643
1677
  mediaKind === "video" &&
1644
1678
  inspection.durationSeconds > MAX_VIDEO_DURATION_SECONDS
@@ -1747,12 +1781,6 @@
1747
1781
  const canvas = document.createElement("canvas");
1748
1782
  canvas.width = Math.max(1, Math.round(inspection.width * scale));
1749
1783
  canvas.height = Math.max(1, Math.round(inspection.height * scale));
1750
- if (canvas.width < MIN_DIMENSION || canvas.height < MIN_DIMENSION) {
1751
- throw validationError(
1752
- "low_resolution",
1753
- "La resolución mínima final es 600 × 600 px.",
1754
- );
1755
- }
1756
1784
  const context = canvas.getContext("2d", {
1757
1785
  alpha: file.type !== "image/jpeg",
1758
1786
  });
@@ -1848,17 +1876,28 @@
1848
1876
  }
1849
1877
  const error = new Error("upload_failed");
1850
1878
  error.safeCode = "upload_failed";
1851
- error.safeMessage =
1852
- "La carga falló tras 3 intentos. Puedes reintentarla.";
1853
1879
  error.safeStage =
1854
1880
  lastError && lastError.safeStage ? lastError.safeStage : "upload";
1881
+ error.safeMessage =
1882
+ error.safeStage === "resolve"
1883
+ ? "No pudimos preparar la carga. Reintenta o abre la carga segura."
1884
+ : error.safeStage === "put"
1885
+ ? "No pudimos enviar el archivo al almacenamiento. Reintenta o abre la carga segura."
1886
+ : "La carga falló tras 3 intentos. Puedes reintentarla.";
1855
1887
  throw error;
1856
1888
  }
1857
1889
 
1858
1890
  async function resolveUpload(file, uploadId) {
1859
1891
  const uploadPage = new URL(state.session.uploadPageUrl);
1860
- if (!safeUploadPageUrl(uploadPage))
1861
- throw new Error("unsafe_upload_page");
1892
+ const resolveUrl = new URL(
1893
+ state.session.resolveEndpoint,
1894
+ uploadPage.origin,
1895
+ );
1896
+ if (
1897
+ !safeResolveEndpointUrl(resolveUrl, uploadPage) ||
1898
+ !safeUploadPageUrl(uploadPage, resolveUrl)
1899
+ )
1900
+ throw new Error("unsafe_upload_session");
1862
1901
  const token =
1863
1902
  uploadPage.searchParams.get("token") ||
1864
1903
  new URLSearchParams(uploadPage.hash.slice(1)).get("token");
@@ -1867,10 +1906,6 @@
1867
1906
  error.safeStage = "resolve";
1868
1907
  throw error;
1869
1908
  }
1870
- const resolveUrl = new URL(
1871
- state.session.resolveEndpoint,
1872
- uploadPage.origin,
1873
- );
1874
1909
  const response = await fetch(resolveUrl.toString(), {
1875
1910
  method: "POST",
1876
1911
  headers: { "content-type": "application/json" },
@@ -1882,6 +1917,7 @@
1882
1917
  fileSize: file.size,
1883
1918
  }),
1884
1919
  credentials: "omit",
1920
+ redirect: "error",
1885
1921
  signal: activeAbortSignal(),
1886
1922
  });
1887
1923
  if (!response.ok) {
@@ -1947,7 +1983,7 @@
1947
1983
  );
1948
1984
  }
1949
1985
 
1950
- function safeUploadPageUrl(url) {
1986
+ function safeUploadPageUrl(url, resolveUrl) {
1951
1987
  if (url.username || url.password) return false;
1952
1988
  if (
1953
1989
  [
@@ -1957,11 +1993,38 @@
1957
1993
  )
1958
1994
  return true;
1959
1995
  return (
1960
- url.protocol === "http:" &&
1961
- ["localhost", "127.0.0.1", "::1"].includes(url.hostname)
1996
+ (url.protocol === "http:" &&
1997
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) ||
1998
+ (url.protocol === "https:" &&
1999
+ /^\/(?:[a-z]{2}\/)?upload-media\/?$/.test(url.pathname) &&
2000
+ isConfiguredResolveEndpoint(resolveUrl))
2001
+ );
2002
+ }
2003
+
2004
+ function isConfiguredResolveEndpoint(url) {
2005
+ return (
2006
+ /^https?:\/\//.test(CONFIGURED_RESOLVE_ENDPOINT) &&
2007
+ url.toString() === CONFIGURED_RESOLVE_ENDPOINT
1962
2008
  );
1963
2009
  }
1964
2010
 
2011
+ function safeResolveEndpointUrl(url, uploadPageUrl) {
2012
+ if (url.username || url.password) return false;
2013
+ if (url.search || url.hash) return false;
2014
+ const officialApi =
2015
+ url.origin === "https://api.productmaker.app" &&
2016
+ url.pathname === "/v1/uploaded-assets/sessions/resolve";
2017
+ const sameOriginWebProxy =
2018
+ url.origin === uploadPageUrl.origin &&
2019
+ url.pathname === "/api/upload-media/resolve";
2020
+ const localApi =
2021
+ url.protocol === "http:" &&
2022
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname) &&
2023
+ url.pathname === "/v1/uploaded-assets/sessions/resolve";
2024
+ const configuredApi = isConfiguredResolveEndpoint(url);
2025
+ return officialApi || sameOriginWebProxy || localApi || configuredApi;
2026
+ }
2027
+
1965
2028
  function putFileWithProgress(url, file, onProgress) {
1966
2029
  return new Promise((resolve, reject) => {
1967
2030
  const request = new XMLHttpRequest();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.0.0",
3
+ "version": "3.0.2",
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"