@productmaker/mcp 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/stdio.js CHANGED
@@ -5,11 +5,88 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable:
5
5
  // src/stdio.ts
6
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
7
 
8
+ // src/auth/downstream-credential.ts
9
+ import { createHash } from "crypto";
10
+ var StaticApiKeyCredential = class {
11
+ static {
12
+ __name(this, "StaticApiKeyCredential");
13
+ }
14
+ apiKey;
15
+ constructor(apiKey) {
16
+ this.apiKey = apiKey;
17
+ }
18
+ async getAccessToken() {
19
+ return this.apiKey;
20
+ }
21
+ fingerprint() {
22
+ return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
23
+ }
24
+ };
25
+
8
26
  // src/server.ts
9
27
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
28
 
29
+ // src/response-body.ts
30
+ var ResponseBodyTooLargeError = class extends Error {
31
+ static {
32
+ __name(this, "ResponseBodyTooLargeError");
33
+ }
34
+ maxBytes;
35
+ constructor(maxBytes) {
36
+ super(`Response body exceeds the ${maxBytes}-byte limit`), this.maxBytes = maxBytes;
37
+ this.name = "ResponseBodyTooLargeError";
38
+ }
39
+ };
40
+ function cancelResponseBody(response) {
41
+ if (!response.body) return;
42
+ void response.body.cancel().catch(() => void 0);
43
+ }
44
+ __name(cancelResponseBody, "cancelResponseBody");
45
+ async function readResponseBytes(response, maxBytes) {
46
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
47
+ throw new RangeError("maxBytes must be a positive safe integer");
48
+ }
49
+ if (!response.body) return new Uint8Array();
50
+ const contentLength = response.headers.get("content-length");
51
+ const declaredLength = contentLength === null ? NaN : Number(contentLength);
52
+ if (Number.isSafeInteger(declaredLength) && declaredLength > maxBytes) {
53
+ cancelResponseBody(response);
54
+ throw new ResponseBodyTooLargeError(maxBytes);
55
+ }
56
+ const reader = response.body.getReader();
57
+ const chunks = [];
58
+ let totalBytes = 0;
59
+ try {
60
+ while (true) {
61
+ const { done, value } = await reader.read();
62
+ if (done) break;
63
+ totalBytes += value.byteLength;
64
+ if (totalBytes > maxBytes) {
65
+ void reader.cancel().catch(() => void 0);
66
+ throw new ResponseBodyTooLargeError(maxBytes);
67
+ }
68
+ chunks.push(value);
69
+ }
70
+ } finally {
71
+ reader.releaseLock();
72
+ }
73
+ const body = new Uint8Array(totalBytes);
74
+ let offset = 0;
75
+ for (const chunk of chunks) {
76
+ body.set(chunk, offset);
77
+ offset += chunk.byteLength;
78
+ }
79
+ return body;
80
+ }
81
+ __name(readResponseBytes, "readResponseBytes");
82
+ async function readResponseText(response, maxBytes) {
83
+ return new TextDecoder().decode(await readResponseBytes(response, maxBytes));
84
+ }
85
+ __name(readResponseText, "readResponseText");
86
+
11
87
  // src/api-client.ts
12
- import { createHash } from "crypto";
88
+ var DOWNSTREAM_REQUEST_TIMEOUT_MS = 3e4;
89
+ var MAX_DOWNSTREAM_RESPONSE_BYTES = 1048576;
13
90
  var ApiHttpError = class extends Error {
14
91
  static {
15
92
  __name(this, "ApiHttpError");
@@ -25,88 +102,95 @@ var ApiClient = class {
25
102
  static {
26
103
  __name(this, "ApiClient");
27
104
  }
28
- apiKey;
29
105
  baseUrl;
30
- constructor(baseUrl, apiKey) {
31
- this.apiKey = apiKey;
106
+ credential;
107
+ signal;
108
+ requestTimeoutMs;
109
+ constructor(baseUrl, credential, options = {}) {
32
110
  this.baseUrl = baseUrl.replace(/\/+$/, "");
111
+ this.credential = credential;
112
+ this.signal = options.signal;
113
+ this.requestTimeoutMs = options.requestTimeoutMs ?? DOWNSTREAM_REQUEST_TIMEOUT_MS;
33
114
  }
34
- authHeaders() {
115
+ async authHeaders() {
35
116
  return {
36
- Authorization: `Bearer ${this.apiKey}`
117
+ Authorization: `Bearer ${await this.credential.getAccessToken()}`
37
118
  };
38
119
  }
39
120
  /**
40
- * Stable per-key identifier. The MCP never sees the userId — the API key is
41
- * the only per-user handle it holds — so this stands in as the user
42
- * component of derived idempotency keys. Never log or return it raw to a
43
- * caller: it is derived from a secret.
121
+ * Stable per-credential identifier. The MCP never sees the userId, so this
122
+ * stands in as the user component of derived idempotency keys. Never log or
123
+ * return the raw credential material to a caller: implementations hash or
124
+ * otherwise derive a safe fingerprint.
44
125
  */
45
126
  fingerprint() {
46
- return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
127
+ return this.credential.fingerprint();
47
128
  }
48
129
  publicUrl(path3) {
49
130
  const normalizedPath = path3.startsWith("/") ? path3 : `/${path3}`;
50
131
  return new URL(`${this.baseUrl}${normalizedPath}`).toString();
51
132
  }
52
- async getJson(path3) {
53
- const res = await fetch(`${this.baseUrl}${path3}`, {
54
- headers: this.authHeaders()
55
- });
133
+ async getJson(path3, options) {
134
+ const res = await this.request(`${this.baseUrl}${path3}`, {
135
+ headers: await this.authHeaders()
136
+ }, options);
56
137
  return this.parse(res);
57
138
  }
58
- async postJson(path3, body) {
59
- const res = await fetch(`${this.baseUrl}${path3}`, {
139
+ async postJson(path3, body, options) {
140
+ const authHeaders = await this.authHeaders();
141
+ const res = await this.request(`${this.baseUrl}${path3}`, {
60
142
  method: "POST",
61
143
  headers: {
62
- ...this.authHeaders(),
144
+ ...authHeaders,
63
145
  "Content-Type": "application/json"
64
146
  },
65
147
  body: JSON.stringify(body)
66
- });
148
+ }, options);
67
149
  return this.parse(res);
68
150
  }
69
- async patchJson(path3, body) {
70
- const res = await fetch(`${this.baseUrl}${path3}`, {
151
+ async patchJson(path3, body, options) {
152
+ const authHeaders = await this.authHeaders();
153
+ const res = await this.request(`${this.baseUrl}${path3}`, {
71
154
  method: "PATCH",
72
155
  headers: {
73
- ...this.authHeaders(),
156
+ ...authHeaders,
74
157
  "Content-Type": "application/json"
75
158
  },
76
159
  body: JSON.stringify(body)
77
- });
160
+ }, options);
78
161
  return this.parse(res);
79
162
  }
80
- async putJson(path3, body) {
81
- const res = await fetch(`${this.baseUrl}${path3}`, {
163
+ async putJson(path3, body, options) {
164
+ const authHeaders = await this.authHeaders();
165
+ const res = await this.request(`${this.baseUrl}${path3}`, {
82
166
  method: "PUT",
83
167
  headers: {
84
- ...this.authHeaders(),
168
+ ...authHeaders,
85
169
  "Content-Type": "application/json"
86
170
  },
87
171
  body: JSON.stringify(body)
88
- });
172
+ }, options);
89
173
  return this.parse(res);
90
174
  }
91
- async deleteJson(path3) {
92
- const res = await fetch(`${this.baseUrl}${path3}`, {
175
+ async deleteJson(path3, options) {
176
+ const res = await this.request(`${this.baseUrl}${path3}`, {
93
177
  method: "DELETE",
94
- headers: this.authHeaders()
95
- });
178
+ headers: await this.authHeaders()
179
+ }, options);
96
180
  return this.parse(res);
97
181
  }
98
182
  /** Upload bytes to a presigned URL; the URL itself does not need API auth. */
99
- async putBytes(url, body, contentType) {
100
- const res = await fetch(url, {
183
+ async putBytes(url, body, contentType, options) {
184
+ const res = await this.request(url, {
101
185
  method: "PUT",
102
186
  headers: {
103
187
  "Content-Type": contentType,
104
188
  "Content-Length": String(body.byteLength)
105
189
  },
106
190
  body: new Uint8Array(body)
107
- });
191
+ }, options);
108
192
  if (!res.ok) {
109
- throw new ApiHttpError(res.status, await res.text().catch(() => ""), `HTTP ${res.status} uploading media`);
193
+ throw new ApiHttpError(res.status, await readResponseText(res, MAX_DOWNSTREAM_RESPONSE_BYTES).catch(() => ""), `HTTP ${res.status} uploading media`);
110
194
  }
111
195
  }
112
196
  async postMultipart(path3, files, fields, options) {
@@ -131,39 +215,62 @@ var ApiClient = class {
131
215
  form.append(k, v);
132
216
  }
133
217
  }
134
- const res = await fetch(`${this.baseUrl}${path3}`, {
218
+ const authHeaders = await this.authHeaders();
219
+ const res = await this.request(`${this.baseUrl}${path3}`, {
135
220
  method: "POST",
136
221
  headers: {
137
- ...this.authHeaders(),
222
+ ...authHeaders,
138
223
  ...options?.idempotencyKey ? {
139
224
  "Idempotency-Key": options.idempotencyKey
140
225
  } : {}
141
226
  },
142
227
  body: form
143
- });
228
+ }, options);
144
229
  return this.parse(res);
145
230
  }
231
+ request(input, init, options) {
232
+ const signals = [
233
+ AbortSignal.timeout(this.requestTimeoutMs),
234
+ this.signal,
235
+ options?.signal
236
+ ].filter((signal2) => signal2 !== void 0);
237
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
238
+ return fetch(input, {
239
+ ...init,
240
+ signal
241
+ });
242
+ }
146
243
  async parse(res) {
244
+ let bodyText;
245
+ try {
246
+ bodyText = await readResponseText(res, MAX_DOWNSTREAM_RESPONSE_BYTES);
247
+ } catch (error) {
248
+ if (!res.ok) {
249
+ throw new ApiHttpError(res.status, "[response body unavailable]", `HTTP ${res.status} from ${res.url}`);
250
+ }
251
+ throw error;
252
+ }
147
253
  if (!res.ok) {
148
254
  let body;
149
255
  try {
150
- body = await res.json();
256
+ body = JSON.parse(bodyText);
151
257
  } catch {
152
- body = await res.text().catch(() => "");
258
+ body = bodyText;
153
259
  }
154
260
  throw new ApiHttpError(res.status, body, `HTTP ${res.status} from ${res.url}`);
155
261
  }
156
- return await res.json();
262
+ return JSON.parse(bodyText);
157
263
  }
158
264
  };
159
265
 
160
266
  // src/index.ts
161
- var MCP_VERSION = "3.1.0";
267
+ var MCP_VERSION = "3.1.1";
162
268
 
163
269
  // src/meta-upload-app.ts
164
270
  import { readFileSync } from "fs";
165
271
  import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
166
272
  var MEDIA_UPLOAD_APP_RESOURCE_URI = "ui://media-upload/v1.html";
273
+ var MEDIA_UPLOAD_APP_DOMAIN = "https://productmaker.app";
167
274
  var MEDIA_UPLOAD_APP_NAME = "ProductMaker media upload";
168
275
  var MEDIA_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
169
276
  var MEDIA_UPLOAD_APP_CSP = {
@@ -209,6 +316,7 @@ function registerMediaUploadAppResource(server, resolveEndpoint) {
209
316
  mimeType: RESOURCE_MIME_TYPE,
210
317
  _meta: {
211
318
  ui: {
319
+ domain: MEDIA_UPLOAD_APP_DOMAIN,
212
320
  csp
213
321
  }
214
322
  }
@@ -220,6 +328,7 @@ function registerMediaUploadAppResource(server, resolveEndpoint) {
220
328
  text: html,
221
329
  _meta: {
222
330
  ui: {
331
+ domain: MEDIA_UPLOAD_APP_DOMAIN,
223
332
  csp
224
333
  }
225
334
  }
@@ -425,8 +534,15 @@ async function resolveImageUrl(rawUrl) {
425
534
  }
426
535
  const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
427
536
  if (!ALLOWED_MIMES.has(ct)) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `content-type ${ct}`);
428
- const buf = Buffer.from(await res.arrayBuffer());
429
- if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
537
+ let buf;
538
+ try {
539
+ buf = Buffer.from(await readResponseBytes(res, MAX_BYTES));
540
+ } catch (error) {
541
+ if (error instanceof ResponseBodyTooLargeError) {
542
+ throw new ResolveImageError("IMAGE_TOO_LARGE", `image URL exceeds ${MAX_BYTES} bytes`);
543
+ }
544
+ throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", "image response could not be read");
545
+ }
430
546
  const sniffed = sniffMime(buf);
431
547
  if (!sniffed || sniffed !== ct) {
432
548
  throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes (${sniffed ?? "unknown"}) do not match content-type (${ct})`);
@@ -6871,6 +6987,16 @@ var DEFAULT_PRICING = {
6871
6987
  ourCostCents: 0.15,
6872
6988
  userPriceCents: 0.3
6873
6989
  },
6990
+ // ⚠️ Both call sites below moved from gemini-3-flash-preview to gemini-3.7-flash on
6991
+ // 2026-08-15 (#1418) and these rates predate that. They are NOT re-measured, and the
6992
+ // numbers are LEFT UNCHANGED by the same owner decision that governs the whole table
6993
+ // (launch pricing is expected to rise; real user billing derives from actual
6994
+ // usage.cost, so these only feed the estimator and the pre-flight quote ceiling).
6995
+ // - product-accuracy: ZERO calls in the last 20 days of prod billing, so there is no
6996
+ // measurement to compare against and the gate may not be reachable at all.
6997
+ // - wearable-product-semantics: measured 0.4303¢/call over 121 prod calls (4,970
6998
+ // prompt tokens) on flash-preview — this 0.11¢ fallback already UNDER-quotes by
6999
+ // ~4x, independently of the model move. Re-measure after #1418 is deployed.
6874
7000
  "openrouter:product-accuracy": {
6875
7001
  ourCostCents: 0.15,
6876
7002
  userPriceCents: 0.3
@@ -9182,13 +9308,21 @@ var WHATSAPP_ONLY_CHANNEL_EXPOSURE = {
9182
9308
  mcp: "unavailable",
9183
9309
  whatsapp: "adapted"
9184
9310
  };
9185
- function defineCapability(name, schemaHash, confirmation, routingDomain, effects, inputSlots, consumes, produces, channelExposure = SHARED_CHANNEL_EXPOSURE) {
9311
+ function defineCapability(name, schemaHash, confirmation, routingDomain, effects, inputSlots, consumes, produces, channelExposure = SHARED_CHANNEL_EXPOSURE, conversationInputs) {
9186
9312
  return {
9187
9313
  name,
9188
9314
  schemaHash,
9189
9315
  confirmation,
9190
9316
  routingDomain,
9191
9317
  inputSlots,
9318
+ conversation: {
9319
+ inputs: conversationInputs ?? inputSlots.map((input) => ({
9320
+ slot: input.name,
9321
+ askBeforePaidExecution: input.required,
9322
+ explicitOmissionAllowed: !input.required,
9323
+ fieldCompletion: "any"
9324
+ }))
9325
+ },
9192
9326
  effects,
9193
9327
  artifacts: {
9194
9328
  consumes,
@@ -9686,6 +9820,11 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9686
9820
  "compareAtPrice",
9687
9821
  "offer",
9688
9822
  "paymentMethods"
9823
+ ], [
9824
+ "price",
9825
+ "offer",
9826
+ "compareAtPrice",
9827
+ "paymentMethods"
9689
9828
  ]),
9690
9829
  slot("product_context", false, [
9691
9830
  "productContext",
@@ -9707,6 +9846,25 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
9707
9846
  "product_reference"
9708
9847
  ], [
9709
9848
  "landing"
9849
+ ], SHARED_CHANNEL_EXPOSURE, [
9850
+ {
9851
+ slot: "product_reference",
9852
+ askBeforePaidExecution: true,
9853
+ explicitOmissionAllowed: false,
9854
+ fieldCompletion: "any"
9855
+ },
9856
+ {
9857
+ slot: "locale",
9858
+ askBeforePaidExecution: true,
9859
+ explicitOmissionAllowed: false,
9860
+ fieldCompletion: "all"
9861
+ },
9862
+ {
9863
+ slot: "commercial_offer",
9864
+ askBeforePaidExecution: true,
9865
+ explicitOmissionAllowed: true,
9866
+ fieldCompletion: "all"
9867
+ }
9710
9868
  ]),
9711
9869
  defineCapability("generate_video_creative", "6bcd073df66023f8108a671b3027e1f35e9596cf9481ddf13a0282b4c56f5f34", "credit_consuming", "creative", [
9712
9870
  "credit_consuming",
@@ -10081,7 +10239,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
10081
10239
  ], [
10082
10240
  "meta_upload_session"
10083
10241
  ], WHATSAPP_ONLY_CHANNEL_EXPOSURE),
10084
- defineCapability("request_media_upload", "d7ccfffe00f8563219fb3e036b07649ff59334e03fc52f6a2f3cb0823d4aa948", "none", "creative", [
10242
+ defineCapability("request_media_upload", "e91cda1b2d9e44cecf28990b8018ce535ae4e9e6594fa80f503a1bf69d7146ee", "none", "creative", [
10085
10243
  "state_change"
10086
10244
  ], [
10087
10245
  slot("media_kind", true, [
@@ -10389,7 +10547,7 @@ async function readResponseWithCap(response) {
10389
10547
  if (next.done) break;
10390
10548
  total += next.value.byteLength;
10391
10549
  if (total > MAX_MEDIA_BYTES) {
10392
- await reader.cancel();
10550
+ void reader.cancel().catch(() => void 0);
10393
10551
  throw new ResolveMediaError("MEDIA_TOO_LARGE", `media URL exceeds ${MAX_MEDIA_BYTES} bytes`);
10394
10552
  }
10395
10553
  chunks.push(Buffer.from(next.value));
@@ -10537,6 +10695,7 @@ async function resolveMediaRef(ref, opts) {
10537
10695
  if (!response.ok) throw new ResolveMediaError("MEDIA_URL_UNAVAILABLE", `media URL returned HTTP ${response.status}`);
10538
10696
  const contentLength = Number(response.headers.get("content-length") ?? 0);
10539
10697
  if (contentLength > MAX_MEDIA_BYTES) {
10698
+ void response.body?.cancel().catch(() => void 0);
10540
10699
  throw new ResolveMediaError("MEDIA_TOO_LARGE", `media URL exceeds ${MAX_MEDIA_BYTES} bytes`);
10541
10700
  }
10542
10701
  const buffer = await readResponseWithCap(response);
@@ -11450,8 +11609,8 @@ function sanitizeFilename(s) {
11450
11609
  return s.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64) || "asset";
11451
11610
  }
11452
11611
  __name(sanitizeFilename, "sanitizeFilename");
11453
- async function fetchTaskOutputs(api, taskId) {
11454
- const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
11612
+ async function fetchTaskOutputs(api, taskId, options) {
11613
+ const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`, options);
11455
11614
  const videos = (raw.videoGenerators ?? []).map((vg) => {
11456
11615
  const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
11457
11616
  kind: v.kind,
@@ -11952,12 +12111,16 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
11952
12111
  });
11953
12112
  server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, incluye enlaces a sus outputs.\n\nSi aparece `pendingUserAction`, el pipeline est\xE1 PARADO: muestra el draft y llama `resolve_pending_action` repitiendo el mismo `kind`. Kinds: `product_reference_set_approval`, `product_reference_confirmation`, `angle_review_approval`, `service_angle_approval` y `service_script_approval`.", GetTaskStatusInput.shape, async (input, extra) => {
11954
12113
  try {
11955
- const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
11956
12114
  const signal = extra?.signal;
12115
+ const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`, {
12116
+ signal
12117
+ }), "fetcher");
11957
12118
  const snapshot = await pollStatus(fetcher, input.waitSeconds ?? 0, signal);
11958
12119
  if (snapshot.status !== "done") return asText(snapshot);
11959
12120
  if (snapshot.kind === "service") {
11960
- const result = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-result`);
12121
+ const result = await api.getJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/service-result`, {
12122
+ signal
12123
+ });
11961
12124
  return {
11962
12125
  content: [
11963
12126
  {
@@ -11977,7 +12140,9 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
11977
12140
  };
11978
12141
  }
11979
12142
  try {
11980
- const rawOutputs = await fetchTaskOutputs(api, input.taskId);
12143
+ const rawOutputs = await fetchTaskOutputs(api, input.taskId, {
12144
+ signal
12145
+ });
11981
12146
  const outputs = proxifyOutputs(rawOutputs, proxyBaseUrl);
11982
12147
  return {
11983
12148
  content: [
@@ -13513,11 +13678,11 @@ var UploadSessionInputBase = z55.object({
13513
13678
  maxFiles: z55.number().int().min(1).max(10).optional().describe("Optional batch limit: image accepts 1-5, video exactly 1, and mixed 1-10.")
13514
13679
  });
13515
13680
  var OpenAiFileReference = z55.object({
13516
- download_url: z55.string().url(),
13681
+ download_url: z55.string().url().optional().describe("Optional host-provided download URL. The App prefers getFileDownloadUrl(file_id) and the server never fetches this URL."),
13517
13682
  file_id: z55.string().min(1),
13518
13683
  mime_type: z55.string().min(1).optional(),
13519
13684
  file_name: z55.string().min(1).optional()
13520
- }).strict();
13685
+ });
13521
13686
  var UploadSessionToolInputShape = {
13522
13687
  ...UploadSessionInputBase.shape,
13523
13688
  files: z55.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.")
@@ -14800,7 +14965,7 @@ __name(registerProductMakerPrompts, "registerProductMakerPrompts");
14800
14965
 
14801
14966
  // src/server.ts
14802
14967
  function createServer(opts) {
14803
- const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
14968
+ const { apiBaseUrl, proxyBaseUrl = null, transport = "http", instructions = PRODUCTMAKER_SERVER_INSTRUCTIONS } = opts;
14804
14969
  const server = new McpServer({
14805
14970
  name: "productmaker",
14806
14971
  version: MCP_VERSION
@@ -14811,9 +14976,11 @@ function createServer(opts) {
14811
14976
  },
14812
14977
  logging: {}
14813
14978
  },
14814
- instructions: PRODUCTMAKER_SERVER_INSTRUCTIONS
14979
+ instructions
14980
+ });
14981
+ const api = new ApiClient(apiBaseUrl, opts.credential, {
14982
+ signal: opts.abortSignal
14815
14983
  });
14816
- const api = new ApiClient(apiBaseUrl, apiKey);
14817
14984
  registerProductMakerPrompts(server);
14818
14985
  registerMediaUploadAppResource(server, api.publicUrl("/v1/uploaded-assets/sessions/resolve"));
14819
14986
  registerProductCostingTools(server);
@@ -14878,7 +15045,7 @@ async function main() {
14878
15045
  process.exit(1);
14879
15046
  }
14880
15047
  const server = createServer({
14881
- apiKey,
15048
+ credential: new StaticApiKeyCredential(apiKey),
14882
15049
  apiBaseUrl,
14883
15050
  transport: "stdio"
14884
15051
  });
@@ -2273,19 +2273,27 @@
2273
2273
  status.status === "pending" ||
2274
2274
  (state.items.length > 0 &&
2275
2275
  status.assets.length < state.items.length);
2276
+ const canContinueInChat = canMessageHost();
2276
2277
  if (partial) {
2277
2278
  requireFallback(
2278
2279
  "Carga parcial verificada. Puedes continuar desde el enlace seguro si algún archivo no terminó.",
2279
2280
  "warning",
2280
2281
  );
2282
+ } else if (!canContinueInChat) {
2283
+ requireFallback(
2284
+ "Carga completada. Este chat no puede continuar desde el widget. Abre el enlace seguro para seguir.",
2285
+ "warning",
2286
+ );
2281
2287
  } else {
2282
2288
  setFallbackVisibility(state.fallbackRequired);
2283
2289
  }
2284
2290
  setSessionStatus(
2285
2291
  partial
2286
2292
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2287
- : "Carga completada.",
2288
- partial ? "warning" : "success",
2293
+ : canContinueInChat
2294
+ ? "Carga completada."
2295
+ : "Carga completada. Abre el enlace seguro para seguir.",
2296
+ partial || !canContinueInChat ? "warning" : "success",
2289
2297
  );
2290
2298
  } catch (error) {
2291
2299
  const expired =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.1.0",
3
+ "version": "3.1.1",
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.",
@@ -53,7 +53,9 @@
53
53
  "@modelcontextprotocol/sdk": "^1.30.0",
54
54
  "express": "^4.19.2",
55
55
  "express-rate-limit": "^7.4.0",
56
+ "jose": "^5.2.0",
56
57
  "pino": "^9.0.0",
58
+ "posthog-node": "^5.24.11",
57
59
  "zod": "^3.23.8"
58
60
  },
59
61
  "devDependencies": {
@@ -64,8 +66,8 @@
64
66
  "tsx": "^4.16.0",
65
67
  "typescript": "^7.0.2",
66
68
  "vitest": "^2.1.9",
67
- "@packages/product-research": "0.1.0",
68
69
  "@packages/product-economics": "0.1.0",
70
+ "@packages/product-research": "0.1.0",
69
71
  "@packages/types": "0.1.0"
70
72
  },
71
73
  "publishConfig": {