@productmaker/mcp 1.16.3 → 2.0.0

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
@@ -12,6 +12,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
 
14
14
  // src/api-client.ts
15
+ import { createHash } from "crypto";
15
16
  var ApiHttpError = class extends Error {
16
17
  static {
17
18
  __name(this, "ApiHttpError");
@@ -38,6 +39,15 @@ var ApiClient = class {
38
39
  Authorization: `Bearer ${this.apiKey}`
39
40
  };
40
41
  }
42
+ /**
43
+ * Stable per-key identifier. The MCP never sees the userId — the API key is
44
+ * the only per-user handle it holds — so this stands in as the user
45
+ * component of derived idempotency keys. Never log or return it raw to a
46
+ * caller: it is derived from a secret.
47
+ */
48
+ fingerprint() {
49
+ return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
50
+ }
41
51
  async getJson(path3) {
42
52
  const res = await fetch(`${this.baseUrl}${path3}`, {
43
53
  headers: this.authHeaders()
@@ -132,7 +142,7 @@ var ApiClient = class {
132
142
  };
133
143
 
134
144
  // src/index.ts
135
- var MCP_VERSION = "1.16.3";
145
+ var MCP_VERSION = "2.0.0";
136
146
 
137
147
  // src/tools/tasks.ts
138
148
  import { z as z39 } from "zod";
@@ -440,9 +450,11 @@ var VideoModel = z.enum([
440
450
  ]);
441
451
  var NarrativeStyle = z.enum([
442
452
  "review",
453
+ "ugc_how_to",
443
454
  "problem_product_cta",
444
455
  "product_showcase",
445
456
  "cinematic",
457
+ "wild_card",
446
458
  "metaphor_product_cta",
447
459
  "wearable_showcase",
448
460
  "animated_problem_pixar",
@@ -461,9 +473,11 @@ var VideoResolution = z.enum([
461
473
  var VideoPreset = z.enum([
462
474
  "auto",
463
475
  "ugc_dynamic",
476
+ "ugc_how_to",
464
477
  "problem_solution",
465
478
  "product_showcase",
466
479
  "cinematic",
480
+ "wild_card",
467
481
  "wearable_showcase",
468
482
  "pixar_problem",
469
483
  "pixar_sassy",
@@ -2451,6 +2465,7 @@ var TASK_CREATIVE_VARIANT_TEXT_LIMITS = {
2451
2465
  recommendationRationale: 600
2452
2466
  };
2453
2467
  var TaskCreativeVariantVideoPreset = VideoPreset.exclude([
2468
+ "problem_solution",
2454
2469
  "ugc_unboxing_chaos",
2455
2470
  "ugc_curiosity_reveal",
2456
2471
  "ugc_listicle_3",
@@ -2563,6 +2578,7 @@ var TaskVariantSummary = z21.object({
2563
2578
  var MAX_LANDING_IMAGES = 20;
2564
2579
  var AutoResolvedPresetSchema = z22.enum([
2565
2580
  "ugc_dynamic",
2581
+ "ugc_how_to",
2566
2582
  "ugc_demo_review",
2567
2583
  "ugc_unboxing_chaos",
2568
2584
  "ugc_curiosity_reveal",
@@ -2571,6 +2587,7 @@ var AutoResolvedPresetSchema = z22.enum([
2571
2587
  "problem_solution",
2572
2588
  "product_showcase",
2573
2589
  "cinematic",
2590
+ "wild_card",
2574
2591
  "wearable_showcase",
2575
2592
  "pixar_problem",
2576
2593
  "pixar_sassy",
@@ -3149,6 +3166,125 @@ var AdminQualityReportListResponse = z24.object({
3149
3166
  var UpdateQualityReportStatusRequest = z24.object({
3150
3167
  status: QualityReportStatus
3151
3168
  });
3169
+ var AdminSupportConversationStatus = z24.enum([
3170
+ "bot_active",
3171
+ "human_pending",
3172
+ "human_active",
3173
+ "closed"
3174
+ ]);
3175
+ var AdminSupportDeliveryStatus = z24.enum([
3176
+ "draft",
3177
+ "pending",
3178
+ "sent",
3179
+ "delivered",
3180
+ "read",
3181
+ "failed",
3182
+ "blocked"
3183
+ ]);
3184
+ var AdminSupportMessage = z24.object({
3185
+ id: z24.string().uuid(),
3186
+ direction: z24.enum([
3187
+ "inbound",
3188
+ "outbound"
3189
+ ]),
3190
+ senderKind: z24.enum([
3191
+ "user",
3192
+ "bot",
3193
+ "admin",
3194
+ "system"
3195
+ ]),
3196
+ messageType: z24.enum([
3197
+ "text",
3198
+ "image",
3199
+ "audio",
3200
+ "document",
3201
+ "interactive",
3202
+ "status"
3203
+ ]),
3204
+ bodyRedacted: z24.string().nullable(),
3205
+ deliveryStatus: AdminSupportDeliveryStatus.nullable(),
3206
+ createdAt: z24.string().datetime()
3207
+ });
3208
+ var AdminSupportTicket = z24.object({
3209
+ id: z24.string().uuid(),
3210
+ category: z24.enum([
3211
+ "how_to",
3212
+ "credits",
3213
+ "billing",
3214
+ "task_status",
3215
+ "task_error",
3216
+ "video_quality",
3217
+ "landing_quality",
3218
+ "image_quality",
3219
+ "feature_request",
3220
+ "other"
3221
+ ]),
3222
+ failureMode: z24.string().nullable(),
3223
+ status: z24.enum([
3224
+ "open",
3225
+ "triaged",
3226
+ "resolved",
3227
+ "rejected"
3228
+ ]),
3229
+ summary: z24.string(),
3230
+ taskId: z24.string().nullable(),
3231
+ mixTaskId: z24.string().nullable(),
3232
+ qualityReportId: z24.string().nullable(),
3233
+ handoffReason: z24.string().nullable(),
3234
+ createdAt: z24.string().datetime(),
3235
+ updatedAt: z24.string().datetime()
3236
+ });
3237
+ var AdminSupportConversation = z24.object({
3238
+ id: z24.string().uuid(),
3239
+ status: AdminSupportConversationStatus,
3240
+ priority: z24.enum([
3241
+ "low",
3242
+ "normal",
3243
+ "high",
3244
+ "urgent"
3245
+ ]),
3246
+ currentIntent: z24.string().nullable(),
3247
+ assignedAdminId: z24.string().nullable(),
3248
+ customerServiceWindowEndsAt: z24.string().datetime().nullable(),
3249
+ lastInboundAt: z24.string().datetime().nullable(),
3250
+ createdAt: z24.string().datetime(),
3251
+ updatedAt: z24.string().datetime(),
3252
+ contact: z24.object({
3253
+ id: z24.string().uuid(),
3254
+ userId: z24.string().nullable(),
3255
+ verifiedAt: z24.string().datetime().nullable(),
3256
+ botMode: z24.enum([
3257
+ "enabled",
3258
+ "paused",
3259
+ "opted_out"
3260
+ ]),
3261
+ locale: z24.string()
3262
+ }),
3263
+ messages: z24.array(AdminSupportMessage),
3264
+ tickets: z24.array(AdminSupportTicket)
3265
+ });
3266
+ var AdminSupportListQuery = z24.object({
3267
+ status: AdminSupportConversationStatus.optional(),
3268
+ take: z24.coerce.number().int().min(1).max(100).default(50),
3269
+ cursorId: z24.string().uuid().optional()
3270
+ }).strict();
3271
+ var AdminSupportListResponse = z24.object({
3272
+ conversations: z24.array(AdminSupportConversation),
3273
+ nextCursor: z24.string().uuid().nullable()
3274
+ });
3275
+ var AdminSupportReplyRequest = z24.object({
3276
+ body: z24.string().trim().min(1).max(4096),
3277
+ idempotencyKey: z24.string().regex(/^[A-Za-z0-9_-]{8,128}$/)
3278
+ }).strict();
3279
+ var AdminSupportEmptyActionRequest = z24.object({}).strict();
3280
+ var AdminSupportActionResponse = z24.object({
3281
+ conversation: AdminSupportConversation
3282
+ });
3283
+ var AdminSupportReplyResponse = z24.object({
3284
+ messageId: z24.string().uuid(),
3285
+ deliveryStatus: AdminSupportDeliveryStatus,
3286
+ duplicate: z24.boolean()
3287
+ });
3152
3288
 
3153
3289
  // ../../packages/types/src/promo.ts
3154
3290
  import { z as z25 } from "zod";
@@ -3984,6 +4120,23 @@ var DEFAULT_PRICING = {
3984
4120
  ourCostCents: 0.4,
3985
4121
  userPriceCents: 0.8
3986
4122
  },
4123
+ // WhatsApp ProductMaker intent proposal. Real OpenRouter usage.cost wins;
4124
+ // this is only the fallback when the relay omits cost.
4125
+ "openrouter:whatsapp-product-agent-intent": {
4126
+ ourCostCents: 0.4,
4127
+ userPriceCents: 0.8
4128
+ },
4129
+ // Internal support calls are cost-only: callers explicitly override user
4130
+ // price to zero. Static COGS estimates apply because OpenAI Responses exposes
4131
+ // token counts but not the provider's dollar charge.
4132
+ "openai:whatsapp-support-classifier": {
4133
+ ourCostCents: 0.1,
4134
+ userPriceCents: 0.2
4135
+ },
4136
+ "openai:whatsapp-support-draft": {
4137
+ ourCostCents: 0.4,
4138
+ userPriceCents: 0.8
4139
+ },
3987
4140
  // Task-derived creative recommendation (angle/persona/script/landing-section).
3988
4141
  // OpenRouter usage normally supplies the exact COGS and the call layer charges
3989
4142
  // 2x. This rule is the fail-closed fallback when usage.cost is unavailable.
@@ -6430,6 +6583,151 @@ var SceneSubjectContractSchema = z37.object({
6430
6583
  }
6431
6584
  });
6432
6585
 
6586
+ // ../../packages/types/src/productmaker-chat-capabilities.ts
6587
+ var PRODUCTMAKER_MCP_CAPABILITIES = [
6588
+ {
6589
+ name: "approve_video_script",
6590
+ confirmation: "credit_consuming",
6591
+ schemaHash: "bab794a9700a8d3f42b97f444a776e7ed4dcd833038c52588f951b50970ff3fa"
6592
+ },
6593
+ {
6594
+ name: "cost_product",
6595
+ confirmation: "none",
6596
+ schemaHash: "6d1f85a999dbdabae4eda406ff4689cdff2016df375b5b6741b91f4308d819af"
6597
+ },
6598
+ {
6599
+ name: "create_creative_variant",
6600
+ confirmation: "credit_consuming",
6601
+ schemaHash: "8feeed1e6e6a14bdcb0a1d9d8b03350ce7d8117d926db49f19ca5c5a8f1e7033"
6602
+ },
6603
+ {
6604
+ name: "create_product_task",
6605
+ confirmation: "credit_consuming",
6606
+ schemaHash: "5346684b007a40f6e8d53cc0b56ebb20aeb7e5ba48cdde43bc9f4e6163ed2d3f"
6607
+ },
6608
+ {
6609
+ name: "edit_task_draft",
6610
+ confirmation: "state_change",
6611
+ schemaHash: "f1763564083dcaac2d1f4d5e6c4625a2d4ec0ea3eea273aadc0fb9d4ab6c7d47"
6612
+ },
6613
+ {
6614
+ name: "estimate_creative_variant",
6615
+ confirmation: "none",
6616
+ schemaHash: "a5601adcb9578319528ca0a96f37a35b613370646a75932d1b3978d593f7e302"
6617
+ },
6618
+ {
6619
+ name: "find_winning_products",
6620
+ confirmation: "credit_consuming",
6621
+ schemaHash: "40f581e270ca668fb5fd1505a1f7cced6f629936e7d8394426aa0c655b9cef4f"
6622
+ },
6623
+ {
6624
+ name: "generate_free_video",
6625
+ confirmation: "credit_consuming",
6626
+ schemaHash: "a6f984d175c1ae5bc5f36ade27ee12c57763192000453c0387cd4cd6a9f1751d"
6627
+ },
6628
+ {
6629
+ name: "generate_image_creatives",
6630
+ confirmation: "credit_consuming",
6631
+ schemaHash: "fb6afcf8571887906340fa0b8dd17196d2814e480b734ad5d374e8810958152e"
6632
+ },
6633
+ {
6634
+ name: "generate_landing",
6635
+ confirmation: "credit_consuming",
6636
+ schemaHash: "539922b8fe611c73a7349b1c3f92632fe21f161d7874a0ef2172abfec5834759"
6637
+ },
6638
+ {
6639
+ name: "generate_video_creative",
6640
+ confirmation: "credit_consuming",
6641
+ schemaHash: "94e90d60ad5d68538c31d880a635c5be69d6d204fd2119139735af83ebf0b94a"
6642
+ },
6643
+ {
6644
+ name: "get_meta_campaign_status",
6645
+ confirmation: "none",
6646
+ schemaHash: "92077241cf4e9d14edefca410e85715cc383b1433081c3461c925f8fc3d6e8ab"
6647
+ },
6648
+ {
6649
+ name: "get_product_research_run",
6650
+ confirmation: "none",
6651
+ schemaHash: "3d0bfa4409daa4a8126d18787c3dc619dbdf2fbb679a2cffe9ffcd1c0d96402f"
6652
+ },
6653
+ {
6654
+ name: "get_task_status",
6655
+ confirmation: "none",
6656
+ schemaHash: "6876d865685dd371ae6c2da2d5a69b657731e5a032aff46ac95b6077e9a6d93e"
6657
+ },
6658
+ {
6659
+ name: "get_tiktok_campaign_status",
6660
+ confirmation: "none",
6661
+ schemaHash: "7a2f90a9a9ba91506fbb57a0a881d5a7b9ca1bb1074b0a754520f02fd81b2801"
6662
+ },
6663
+ {
6664
+ name: "get_video_script",
6665
+ confirmation: "none",
6666
+ schemaHash: "c96e7042368ba6f241773d26d4a443894b092801d1c81054f3798865138edfba"
6667
+ },
6668
+ {
6669
+ name: "list_meta_ad_accounts",
6670
+ confirmation: "none",
6671
+ schemaHash: "7a014b717a77aac971ea0ab5c0ff47bb13e6cae7ef9442d9dedb15ce36381b15"
6672
+ },
6673
+ {
6674
+ name: "list_product_research_runs",
6675
+ confirmation: "none",
6676
+ schemaHash: "3b8fac903cd9e30c5b6a6c584cc70a819af5392bbdbb32b69eab4c65dc52b556"
6677
+ },
6678
+ {
6679
+ name: "list_shopify_products",
6680
+ confirmation: "none",
6681
+ schemaHash: "152b137e01066f3b443987ad75f5be00de8696ce984144bd775416192be20667"
6682
+ },
6683
+ {
6684
+ name: "list_shopify_shops",
6685
+ confirmation: "none",
6686
+ schemaHash: "7a014b717a77aac971ea0ab5c0ff47bb13e6cae7ef9442d9dedb15ce36381b15"
6687
+ },
6688
+ {
6689
+ name: "list_tasks",
6690
+ confirmation: "none",
6691
+ schemaHash: "cac5e9dda34b90550024cc546976fab1d6de4fc8fca85a3ec6abde65223cb56c"
6692
+ },
6693
+ {
6694
+ name: "list_tiktok_advertisers",
6695
+ confirmation: "none",
6696
+ schemaHash: "7a014b717a77aac971ea0ab5c0ff47bb13e6cae7ef9442d9dedb15ce36381b15"
6697
+ },
6698
+ {
6699
+ name: "publish_creatives_to_meta",
6700
+ confirmation: "external_mutation",
6701
+ schemaHash: "ddb45071d61746b443124346e7a82095315a2a80bd393c73a45f12b895939cf5"
6702
+ },
6703
+ {
6704
+ name: "publish_to_meta",
6705
+ confirmation: "external_mutation",
6706
+ schemaHash: "4b6daa1a25e739ccf208a6acede067ec1c22b1ea5509f332361a17dd46f6bb0b"
6707
+ },
6708
+ {
6709
+ name: "publish_to_shopify",
6710
+ confirmation: "external_mutation",
6711
+ schemaHash: "ec37111aac872e6be22f34c8b547c1e4b58875f798c48e24ca9ca067a0a22d8b"
6712
+ },
6713
+ {
6714
+ name: "publish_to_tiktok",
6715
+ confirmation: "external_mutation",
6716
+ schemaHash: "4f8dccba51b99687f04e1a58e3fde003b6f52ec6c891afe74b1d81d4b6992908"
6717
+ },
6718
+ {
6719
+ name: "regenerate_video_script",
6720
+ confirmation: "credit_consuming",
6721
+ schemaHash: "745687cb41f904e293105f93bbe6bc0471f63fe097ab8f85d3df8d9833084844"
6722
+ },
6723
+ {
6724
+ name: "resolve_pending_action",
6725
+ confirmation: "credit_consuming",
6726
+ schemaHash: "3a9f33a26fce63b20bece3a528f8eb16e917dabc8a9ec5f04e746bad84757aa3"
6727
+ }
6728
+ ];
6729
+ var PRODUCTMAKER_MCP_CAPABILITY_NAMES = PRODUCTMAKER_MCP_CAPABILITIES.map((capability) => capability.name);
6730
+
6433
6731
  // src/resolve-media.ts
6434
6732
  var MAX_MEDIA_BYTES = CAMPAIGN_LIMITS.MAX_FILE_SIZE_BYTES;
6435
6733
  var MEDIA_FORMATS = [
@@ -6689,20 +6987,22 @@ var LandingImageFormatEnum = z38.enum([
6689
6987
  var VideoPresetEnum = z38.enum([
6690
6988
  "auto",
6691
6989
  "ugc_dynamic",
6990
+ "ugc_how_to",
6692
6991
  "ugc_unboxing_chaos",
6693
- "problem_solution",
6694
6992
  "product_showcase",
6695
6993
  "wearable_showcase",
6696
6994
  "cinematic",
6995
+ "wild_card",
6697
6996
  "pixar_problem",
6698
6997
  "pixar_sassy"
6699
6998
  ]);
6700
6999
  var TaskCreativeVariantVideoPresetEnum = z38.enum([
6701
7000
  "auto",
6702
7001
  "ugc_dynamic",
6703
- "problem_solution",
7002
+ "ugc_how_to",
6704
7003
  "product_showcase",
6705
7004
  "cinematic",
7005
+ "wild_card",
6706
7006
  "wearable_showcase",
6707
7007
  "pixar_problem",
6708
7008
  "pixar_sassy",
@@ -6715,15 +7015,15 @@ var VIDEO_PRESET_RESOLUTION = {
6715
7015
  narrativeStyle: "review",
6716
7016
  videoModel: "veo"
6717
7017
  },
6718
- ugc_unboxing_chaos: {
7018
+ ugc_how_to: {
6719
7019
  videoStyle: "ai_dynamic",
6720
- narrativeStyle: "ugc_unboxing_chaos",
7020
+ narrativeStyle: "ugc_how_to",
6721
7021
  videoModel: "veo"
6722
7022
  },
6723
- problem_solution: {
7023
+ ugc_unboxing_chaos: {
6724
7024
  videoStyle: "ai_dynamic",
6725
- narrativeStyle: "problem_product_cta",
6726
- videoModel: "kling"
7025
+ narrativeStyle: "ugc_unboxing_chaos",
7026
+ videoModel: "veo"
6727
7027
  },
6728
7028
  pixar_problem: {
6729
7029
  videoStyle: "ai_dynamic",
@@ -6749,6 +7049,11 @@ var VIDEO_PRESET_RESOLUTION = {
6749
7049
  videoStyle: "product_only",
6750
7050
  narrativeStyle: "cinematic",
6751
7051
  videoModel: "grok"
7052
+ },
7053
+ wild_card: {
7054
+ videoStyle: "product_only",
7055
+ narrativeStyle: "wild_card",
7056
+ videoModel: "grok"
6752
7057
  }
6753
7058
  };
6754
7059
  function resolveVideoPreset(preset) {
@@ -6783,9 +7088,11 @@ var VideoStyleEnum = z38.enum([
6783
7088
  ]);
6784
7089
  var NarrativeStyleEnum = z38.enum([
6785
7090
  "review",
7091
+ "ugc_how_to",
6786
7092
  "problem_product_cta",
6787
7093
  "product_showcase",
6788
7094
  "cinematic",
7095
+ "wild_card",
6789
7096
  "metaphor_product_cta",
6790
7097
  "wearable_showcase",
6791
7098
  "animated_problem_pixar",
@@ -6798,11 +7105,12 @@ var NarrativeStyleEnum = z38.enum([
6798
7105
  ]);
6799
7106
  var PublicNarrativeStyleEnum = z38.enum([
6800
7107
  "review",
7108
+ "ugc_how_to",
6801
7109
  "ugc_unboxing_chaos",
6802
- "problem_product_cta",
6803
7110
  "product_showcase",
6804
7111
  "wearable_showcase",
6805
7112
  "cinematic",
7113
+ "wild_card",
6806
7114
  "animated_problem_pixar",
6807
7115
  "sassy_product_pixar"
6808
7116
  ]);
@@ -6930,6 +7238,28 @@ async function submitCreative(api, opts) {
6930
7238
  });
6931
7239
  }
6932
7240
  __name(submitCreative, "submitCreative");
7241
+ var ResearchTargetCountryEnum = z38.enum([
7242
+ "CO",
7243
+ "MX",
7244
+ "PE",
7245
+ "EC",
7246
+ "CL"
7247
+ ]);
7248
+ var ResearchCategoryEnum = z38.enum([
7249
+ "auto",
7250
+ "automotive",
7251
+ "kitchen",
7252
+ "pets",
7253
+ "home_organization",
7254
+ "beauty",
7255
+ "wellness",
7256
+ "fitness",
7257
+ "baby",
7258
+ "tech",
7259
+ "tools",
7260
+ "garden",
7261
+ "cleaning"
7262
+ ]);
6933
7263
 
6934
7264
  // src/errors.ts
6935
7265
  var McpErrorCode = {
@@ -6961,6 +7291,7 @@ var McpErrorCode = {
6961
7291
  TASK_NOT_READY: "TASK_NOT_READY",
6962
7292
  CREATIVE_VARIANT_FAILED: "CREATIVE_VARIANT_FAILED",
6963
7293
  CREATIVE_VARIANT_DISPATCH_FAILED: "CREATIVE_VARIANT_DISPATCH_FAILED",
7294
+ PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT",
6964
7295
  // Connection presence
6965
7296
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
6966
7297
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
@@ -7082,12 +7413,13 @@ var CODE_MESSAGES = {
7082
7413
  TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
7083
7414
  CREATIVE_VARIANT_FAILED: "The previous creative variant attempt failed. Ask for confirmation before starting a new paid attempt with a new clientRequestId.",
7084
7415
  CREATIVE_VARIANT_DISPATCH_FAILED: "The creative variant could not be queued. Retry as a new paid attempt with a new clientRequestId.",
7416
+ PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "A product research run with the same country and category is already running. Check it with `list_product_research_runs` instead of starting a new paid run.",
7085
7417
  // API key
7086
7418
  INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
7087
7419
  REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
7088
7420
  // Rate
7089
7421
  RATE_LIMITED: "Too many requests. Wait a few seconds.",
7090
- INSUFFICIENT_CREDITS: "Insufficient credits to create this variant. Check your balance and retry after adding credits.",
7422
+ INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
7091
7423
  TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
7092
7424
  // Generic
7093
7425
  UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
@@ -7100,6 +7432,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
7100
7432
  McpErrorCode.INVALID_INPUT,
7101
7433
  McpErrorCode.INSUFFICIENT_CREDITS,
7102
7434
  McpErrorCode.TASK_QUOTA_EXCEEDED,
7435
+ McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
7103
7436
  McpErrorCode.INVALID_API_KEY,
7104
7437
  McpErrorCode.REVOKED_API_KEY,
7105
7438
  McpErrorCode.IMAGE_PATH_NOT_FOUND,
@@ -7162,6 +7495,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
7162
7495
  McpErrorCode.INSUFFICIENT_CREDITS,
7163
7496
  McpErrorCode.TASK_QUOTA_EXCEEDED,
7164
7497
  McpErrorCode.CREATIVE_VARIANT_DISPATCH_FAILED,
7498
+ McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
7165
7499
  // Shopify
7166
7500
  McpErrorCode.SHOPIFY_NOT_CONNECTED,
7167
7501
  McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
@@ -7275,9 +7609,11 @@ function translateError(err) {
7275
7609
  const body = isRecord(err.body) ? err.body : {};
7276
7610
  if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
7277
7611
  const code = body.code;
7278
- return mcp(code, resolveMessage(code, body.message), body.helpUrl ? {
7279
- helpUrl: body.helpUrl
7280
- } : {});
7612
+ const extra = {};
7613
+ if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
7614
+ if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
7615
+ if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
7616
+ return mcp(code, resolveMessage(code, body.message), extra);
7281
7617
  }
7282
7618
  if (err.status === 401) {
7283
7619
  return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
@@ -7533,7 +7869,7 @@ var CreateProductTaskInput = z39.object({
7533
7869
  paymentMethods: z39.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
7534
7870
  productContext: z39.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
7535
7871
  productLabel: z39.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
7536
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s; 30s usa el ritmo Multiescena. Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso), "problem_solution" (problema\u2192producto\u2192CTA), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla), "product_showcase" (solo producto, sin actor), "wearable_showcase" (ropa/accesorios puestos). Si el usuario pide algo fuera de esta lista (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.'),
7872
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla). Si el usuario pide algo fuera de esta lista, p\xEDdele una opci\xF3n v\xE1lida.'),
7537
7873
  durationSeconds: z39.union([
7538
7874
  z39.literal(15),
7539
7875
  z39.literal(30)
@@ -7542,7 +7878,7 @@ var CreateProductTaskInput = z39.object({
7542
7878
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
7543
7879
  videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez (~el doble de precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
7544
7880
  multiAngleVideos: z39.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
7545
- angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_unboxing_chaos", "problem_product_cta", "product_showcase", "wearable_showcase", "cinematic", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
7881
+ angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_how_to", "ugc_unboxing_chaos", "product_showcase", "wearable_showcase", "cinematic", "wild_card", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
7546
7882
  outputs: z39.array(z39.enum([
7547
7883
  "landing",
7548
7884
  "video",
@@ -8028,7 +8364,7 @@ var VideoInput = z40.object({
8028
8364
  z40.literal(15),
8029
8365
  z40.literal(30)
8030
8366
  ]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
8031
- videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan tanto a 15s como a 30s; el formato de 30s conserva el ritmo multi-escena. Opciones: "auto", "ugc_dynamic" (UGC cl\xE1sico testimonial multi-hook), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "cinematic" (historia visual premium), "pixar_problem" (problema animado 3D estilo Pixar), "pixar_sassy" (producto que habla con personalidad), "product_showcase" (sin actor, solo producto), "wearable_showcase" (ropa/accesorios puestos).'),
8367
+ videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla. Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s. Opciones: "auto", "ugc_dynamic" (review con experiencia y prueba), "ugc_how_to" (tutorial con pasos y resultado visible), "ugc_unboxing_chaos" (apertura y primera prueba), "product_showcase" (producto ejecutando una funci\xF3n real), "wearable_showcase" (prenda o accesorio modelado), "cinematic" (TV spot con receta cinematogr\xE1fica), "wild_card" (una regla visual experimental original), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla).'),
8032
8368
  videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista"; para durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
8033
8369
  videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "720p" = m\xE1xima nitidez (RECOMENDADO, default si se omite); "480p" = render m\xE1s econ\xF3mico. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p).'),
8034
8370
  features: z40.array(z40.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
@@ -9645,6 +9981,224 @@ function formatPercent(value) {
9645
9981
  }
9646
9982
  __name(formatPercent, "formatPercent");
9647
9983
 
9984
+ // src/tools/product-research.ts
9985
+ import { z as z49 } from "zod";
9986
+
9987
+ // src/tools/product-research-idempotency.ts
9988
+ import { createHash as createHash2 } from "crypto";
9989
+ var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
9990
+ var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
9991
+ function uuidV5(name, namespace) {
9992
+ const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
9993
+ const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
9994
+ const bytes = Buffer.from(digest.subarray(0, 16));
9995
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
9996
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
9997
+ const hex = bytes.toString("hex");
9998
+ return [
9999
+ hex.slice(0, 8),
10000
+ hex.slice(8, 12),
10001
+ hex.slice(12, 16),
10002
+ hex.slice(16, 20),
10003
+ hex.slice(20)
10004
+ ].join("-");
10005
+ }
10006
+ __name(uuidV5, "uuidV5");
10007
+ function deriveIdempotencyKey(input) {
10008
+ const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
10009
+ const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
10010
+ return uuidV5(name, NAMESPACE);
10011
+ }
10012
+ __name(deriveIdempotencyKey, "deriveIdempotencyKey");
10013
+
10014
+ // src/tools/product-research-render.ts
10015
+ var num = /* @__PURE__ */ __name((value, suffix = "") => value === void 0 ? "\u2014" : `${Math.round(value * 100) / 100}${suffix}`, "num");
10016
+ function renderMeta(meta) {
10017
+ if (meta.status === "evidence_failed") {
10018
+ return [
10019
+ "- \u26A0\uFE0F **Sin evidencia publicitaria:** no se pudo consultar Meta Ads para este producto. La puntuaci\xF3n se apoya s\xF3lo en datos de venta."
10020
+ ];
10021
+ }
10022
+ if (meta.status === "evidence_empty") {
10023
+ return [
10024
+ "- \u26A0\uFE0F **Sin evidencia publicitaria:** no se encontraron anuncios activos en Meta. Puede ser un mercado virgen o una b\xFAsqueda sin cobertura; la puntuaci\xF3n se apoya s\xF3lo en datos de venta."
10025
+ ];
10026
+ }
10027
+ return [
10028
+ `- **Evidencia en Meta Ads:** ${meta.totalAds} anuncios de ${meta.distinctAdvertisers} anunciantes, m\xE1ximo ${meta.maxActiveDays} d\xEDas activos, oportunidad de \xE1ngulo ${meta.angleOpportunity}/100.`,
10029
+ meta.summary ? ` ${meta.summary}` : ""
10030
+ ].filter(Boolean);
10031
+ }
10032
+ __name(renderMeta, "renderMeta");
10033
+ function renderCandidate(candidate, index) {
10034
+ const name = candidate.nameLocal ?? candidate.nameOriginal;
10035
+ const lines = [
10036
+ `### ${index + 1}. ${name}`,
10037
+ `**${candidate.score}/100 \xB7 ${candidate.label} \xB7 confianza ${candidate.confidence}**`
10038
+ ];
10039
+ if (candidate.insight) lines.push("", candidate.insight);
10040
+ lines.push("", "**Por qu\xE9:**");
10041
+ lines.push(...candidate.reasons.map((reason) => `- ${reason}`));
10042
+ lines.push("", "**Riesgos:**");
10043
+ lines.push(...candidate.risks.length > 0 ? candidate.risks.map((risk) => `- ${risk}`) : [
10044
+ "- No se identificaron riesgos espec\xEDficos."
10045
+ ]);
10046
+ lines.push("", ...renderMeta(candidate.meta));
10047
+ lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
10048
+ if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
10049
+ return lines.join("\n");
10050
+ }
10051
+ __name(renderCandidate, "renderCandidate");
10052
+ function renderResearchReport(run) {
10053
+ if (run.status === "failed" || run.status === "refunded") {
10054
+ return [
10055
+ `\u274C **La investigaci\xF3n no pudo completarse** (estado: ${run.status}).`,
10056
+ run.errorCode ? `C\xF3digo: \`${run.errorCode}\`` : "",
10057
+ run.refunded ? `Los ${run.priceCredits} cr\xE9ditos fueron reembolsados.` : "\u26A0\uFE0F No consta reembolso \u2014 rev\xEDsalo antes de reintentar."
10058
+ ].filter(Boolean).join("\n");
10059
+ }
10060
+ if (run.status === "ready" && run.result === null) {
10061
+ return [
10062
+ `\u274C **No se pudo leer el reporte** (estado: ${run.status}).`,
10063
+ `El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3, pero el reporte qued\xF3 corrupto y no puede recuperarse.`,
10064
+ `Contacta a soporte con este runId para revisar un reembolso: \`${run.id}\`.`
10065
+ ].join("\n");
10066
+ }
10067
+ if (run.result === null) {
10068
+ return [
10069
+ `\u23F3 **La investigaci\xF3n sigue en curso** (estado: ${run.status}, etapa: ${run.progressStage}).`,
10070
+ `El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3; el reporte no se pierde.`,
10071
+ `Cons\xFAltalo con \`get_product_research_run\` usando runId \`${run.id}\`.`
10072
+ ].join("\n");
10073
+ }
10074
+ const { result } = run;
10075
+ const degraded = result.candidates.filter((candidate) => candidate.meta.status !== "evidence_available").length;
10076
+ const header = [
10077
+ `\u2705 **Productos ganadores \xB7 ${result.targetCountry} \xB7 ${result.categoryKey}**`,
10078
+ "",
10079
+ `Analizados ${result.methodology.kaloCandidateCount} candidatos del mercado ${result.sourceMarket}; ${result.methodology.metaCoveredCandidateCount} con cobertura de Meta Ads. Datos con ${num(result.freshnessHours, " h")} de antig\xFCedad.`
10080
+ ];
10081
+ if (degraded > 0) {
10082
+ header.push("", `\u26A0\uFE0F ${degraded} de ${result.candidates.length} productos no tienen evidencia publicitaria. Su puntuaci\xF3n se apoya s\xF3lo en datos de venta.`);
10083
+ }
10084
+ return [
10085
+ ...header,
10086
+ "",
10087
+ ...result.candidates.map((candidate, index) => renderCandidate(candidate, index))
10088
+ ].join("\n\n");
10089
+ }
10090
+ __name(renderResearchReport, "renderResearchReport");
10091
+
10092
+ // src/tools/product-research.ts
10093
+ var PRICE_CREDITS = 2e3;
10094
+ var TERMINAL2 = /* @__PURE__ */ new Set([
10095
+ "ready",
10096
+ "failed",
10097
+ "refunded"
10098
+ ]);
10099
+ var POLL_INTERVAL_MS = 5e3;
10100
+ var MAX_WAIT_SECONDS = 120;
10101
+ var FindInput = z49.object({
10102
+ targetCountry: ResearchTargetCountryEnum.describe("Pa\xEDs objetivo. S\xF3lo CO, MX, PE, EC y CL tienen cobertura de investigaci\xF3n."),
10103
+ categoryKey: ResearchCategoryEnum.default("auto").describe('Categor\xEDa a investigar. Usa "auto" para que la IA elija seg\xFAn el mercado.'),
10104
+ confirmed: z49.boolean().describe(`Ponlo en true SOLO despu\xE9s de que el usuario haya aceptado el cargo de ${PRICE_CREDITS} cr\xE9ditos. No lo asumas.`),
10105
+ waitSeconds: z49.number().int().min(0).max(MAX_WAIT_SECONDS).optional().describe("Segundos a esperar el reporte antes de devolver el runId. M\xE1ximo 120.")
10106
+ });
10107
+ var GetInput = z49.object({
10108
+ runId: z49.string().uuid().describe("ID del run devuelto por find_winning_products.")
10109
+ });
10110
+ var ListInput = z49.object({
10111
+ limit: z49.number().int().min(1).max(20).default(10)
10112
+ });
10113
+ function textResult(text) {
10114
+ return {
10115
+ content: [
10116
+ {
10117
+ type: "text",
10118
+ text
10119
+ }
10120
+ ]
10121
+ };
10122
+ }
10123
+ __name(textResult, "textResult");
10124
+ var sleep2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)), "sleep");
10125
+ async function pollUntilTerminal(api, runId, waitSeconds) {
10126
+ const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
10127
+ let run = await api.getJson(`/v1/product-research/runs/${runId}`);
10128
+ while (!TERMINAL2.has(run.status) && Date.now() < deadline) {
10129
+ await sleep2(POLL_INTERVAL_MS);
10130
+ run = await api.getJson(`/v1/product-research/runs/${runId}`);
10131
+ }
10132
+ return run;
10133
+ }
10134
+ __name(pollUntilTerminal, "pollUntilTerminal");
10135
+ function registerProductResearchTools(server, api) {
10136
+ server.tool("find_winning_products", `Busca productos ganadores de dropshipping para un pa\xEDs y categor\xEDa, y explica POR QU\xC9 la IA eligi\xF3 cada uno (razones, riesgos y evidencia real de anuncios en Meta Ads). CUESTA ${PRICE_CREDITS} CR\xC9DITOS por ejecuci\xF3n. ANTES de invocar, dile al usuario el costo y espera su aceptaci\xF3n expl\xEDcita; s\xF3lo entonces pasa confirmed: true. Consulta primero list_product_research_runs (GRATIS) \u2014 puede que el usuario ya tenga un reporte pagado para ese pa\xEDs/categor\xEDa y no necesites cobrar de nuevo. Pa\xEDses disponibles: CO, MX, PE, EC, CL. Tarda varios minutos: usa waitSeconds para esperar, y si expira consulta el resultado con get_product_research_run \u2014 el reporte NO se pierde.`, FindInput.shape, async (input) => {
10137
+ const parsed = FindInput.parse(input);
10138
+ if (!parsed.confirmed) {
10139
+ throw mcp(McpErrorCode.INVALID_INPUT, `This research run costs ${PRICE_CREDITS} credits. Tell the user the cost, get an explicit yes, then call again with confirmed: true.`);
10140
+ }
10141
+ try {
10142
+ const idempotencyKey = deriveIdempotencyKey({
10143
+ fingerprint: api.fingerprint(),
10144
+ targetCountry: parsed.targetCountry,
10145
+ categoryKey: parsed.categoryKey,
10146
+ nowMs: Date.now()
10147
+ });
10148
+ const created = await api.postJson("/v1/product-research/runs", {
10149
+ targetCountry: parsed.targetCountry,
10150
+ categoryKey: parsed.categoryKey,
10151
+ idempotencyKey
10152
+ });
10153
+ const waitSeconds = parsed.waitSeconds ?? 0;
10154
+ let run = created;
10155
+ if (waitSeconds > 0 && !TERMINAL2.has(created.status)) {
10156
+ try {
10157
+ run = await pollUntilTerminal(api, created.id, waitSeconds);
10158
+ } catch {
10159
+ run = created;
10160
+ }
10161
+ }
10162
+ const notice = created.reused ? `
10163
+
10164
+ _Esta llamada no cobr\xF3 cr\xE9ditos: se reutiliz\xF3 una investigaci\xF3n reciente con los mismos par\xE1metros. Para investigar una categor\xEDa distinta, vuelve a llamar con un \`categoryKey\` expl\xEDcito \u2014 eso inicia una investigaci\xF3n NUEVA con un cargo NUEVO de ${PRICE_CREDITS} cr\xE9ditos, as\xED que pide confirmaci\xF3n expl\xEDcita al usuario antes de hacerlo._` : "";
10165
+ return textResult(`${renderResearchReport(run)}${notice}`);
10166
+ } catch (error) {
10167
+ throw translateError(error);
10168
+ }
10169
+ });
10170
+ server.tool("get_product_research_run", "Devuelve el reporte completo de una investigaci\xF3n de productos ganadores: score, veredicto, confianza, razones, riesgos y evidencia de Meta Ads por producto. GRATIS y read-only. \xDAsalo cuando find_winning_products expir\xF3 la espera, o para releer un reporte anterior encontrado con list_product_research_runs.", GetInput.shape, async (input) => {
10171
+ const parsed = GetInput.parse(input);
10172
+ try {
10173
+ const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
10174
+ return {
10175
+ content: [
10176
+ {
10177
+ type: "text",
10178
+ text: JSON.stringify(run)
10179
+ },
10180
+ {
10181
+ type: "text",
10182
+ text: renderResearchReport(run)
10183
+ }
10184
+ ]
10185
+ };
10186
+ } catch (error) {
10187
+ throw translateError(error);
10188
+ }
10189
+ });
10190
+ server.tool("list_product_research_runs", "Lista las investigaciones de productos ganadores ya pagadas por el usuario, de la m\xE1s reciente a la m\xE1s antigua. GRATIS y read-only. Cons\xFAltalo ANTES de lanzar una nueva: un reporte ya pagado se relee gratis con get_product_research_run.", ListInput.shape, async (input) => {
10191
+ const parsed = ListInput.parse(input);
10192
+ try {
10193
+ const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
10194
+ return textResult(JSON.stringify(runs));
10195
+ } catch (error) {
10196
+ throw translateError(error);
10197
+ }
10198
+ });
10199
+ }
10200
+ __name(registerProductResearchTools, "registerProductResearchTools");
10201
+
9648
10202
  // src/server.ts
9649
10203
  function createServer(opts) {
9650
10204
  const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
@@ -9661,6 +10215,7 @@ function createServer(opts) {
9661
10215
  });
9662
10216
  const api = new ApiClient(apiBaseUrl, apiKey);
9663
10217
  registerProductCostingTools(server);
10218
+ registerProductResearchTools(server, api);
9664
10219
  registerTaskTools(server, api, {
9665
10220
  proxyBaseUrl,
9666
10221
  transport