@productmaker/mcp 1.16.3 → 1.17.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/README.md +3 -0
- package/dist/http.js +398 -5
- package/dist/stdio.js +398 -5
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -86,6 +86,9 @@ Sign up at [productmaker.app](https://productmaker.app) and create a key (prefix
|
|
|
86
86
|
| `publish_to_tiktok` | Create a TikTok Ads campaign from a task (always created PAUSED). Same `angleIndexes` + `campaignConfig` shape as Meta |
|
|
87
87
|
| `get_meta_campaign_status` | Read-only status + lifetime insights (spend/impressions/clicks/reach) of a Meta campaign |
|
|
88
88
|
| `get_tiktok_campaign_status` | Read-only status + lifetime report of a TikTok campaign (requires `advertiserId`) |
|
|
89
|
+
| `find_winning_products` | Research winning dropshipping products for a country/category with AI-explained reasons, risks, and real Meta Ads evidence. **Costs 2000 credits per run** — requires explicit user confirmation (`confirmed: true`) before charging. Covers CO, MX, PE, EC, CL |
|
|
90
|
+
| `get_product_research_run` | Read the full report (score, verdict, confidence, reasons, risks, ad evidence) of a research run by `runId`. Free |
|
|
91
|
+
| `list_product_research_runs` | List the user's past research runs, most recent first. Free |
|
|
89
92
|
|
|
90
93
|
## Video duration and winner review
|
|
91
94
|
|
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.
|
|
145
|
+
var MCP_VERSION = "1.17.0";
|
|
136
146
|
|
|
137
147
|
// src/tools/tasks.ts
|
|
138
148
|
import { z as z39 } from "zod";
|
|
@@ -3149,6 +3159,125 @@ var AdminQualityReportListResponse = z24.object({
|
|
|
3149
3159
|
var UpdateQualityReportStatusRequest = z24.object({
|
|
3150
3160
|
status: QualityReportStatus
|
|
3151
3161
|
});
|
|
3162
|
+
var AdminSupportConversationStatus = z24.enum([
|
|
3163
|
+
"bot_active",
|
|
3164
|
+
"human_pending",
|
|
3165
|
+
"human_active",
|
|
3166
|
+
"closed"
|
|
3167
|
+
]);
|
|
3168
|
+
var AdminSupportDeliveryStatus = z24.enum([
|
|
3169
|
+
"draft",
|
|
3170
|
+
"pending",
|
|
3171
|
+
"sent",
|
|
3172
|
+
"delivered",
|
|
3173
|
+
"read",
|
|
3174
|
+
"failed",
|
|
3175
|
+
"blocked"
|
|
3176
|
+
]);
|
|
3177
|
+
var AdminSupportMessage = z24.object({
|
|
3178
|
+
id: z24.string().uuid(),
|
|
3179
|
+
direction: z24.enum([
|
|
3180
|
+
"inbound",
|
|
3181
|
+
"outbound"
|
|
3182
|
+
]),
|
|
3183
|
+
senderKind: z24.enum([
|
|
3184
|
+
"user",
|
|
3185
|
+
"bot",
|
|
3186
|
+
"admin",
|
|
3187
|
+
"system"
|
|
3188
|
+
]),
|
|
3189
|
+
messageType: z24.enum([
|
|
3190
|
+
"text",
|
|
3191
|
+
"image",
|
|
3192
|
+
"audio",
|
|
3193
|
+
"document",
|
|
3194
|
+
"interactive",
|
|
3195
|
+
"status"
|
|
3196
|
+
]),
|
|
3197
|
+
bodyRedacted: z24.string().nullable(),
|
|
3198
|
+
deliveryStatus: AdminSupportDeliveryStatus.nullable(),
|
|
3199
|
+
createdAt: z24.string().datetime()
|
|
3200
|
+
});
|
|
3201
|
+
var AdminSupportTicket = z24.object({
|
|
3202
|
+
id: z24.string().uuid(),
|
|
3203
|
+
category: z24.enum([
|
|
3204
|
+
"how_to",
|
|
3205
|
+
"credits",
|
|
3206
|
+
"billing",
|
|
3207
|
+
"task_status",
|
|
3208
|
+
"task_error",
|
|
3209
|
+
"video_quality",
|
|
3210
|
+
"landing_quality",
|
|
3211
|
+
"image_quality",
|
|
3212
|
+
"feature_request",
|
|
3213
|
+
"other"
|
|
3214
|
+
]),
|
|
3215
|
+
failureMode: z24.string().nullable(),
|
|
3216
|
+
status: z24.enum([
|
|
3217
|
+
"open",
|
|
3218
|
+
"triaged",
|
|
3219
|
+
"resolved",
|
|
3220
|
+
"rejected"
|
|
3221
|
+
]),
|
|
3222
|
+
summary: z24.string(),
|
|
3223
|
+
taskId: z24.string().nullable(),
|
|
3224
|
+
mixTaskId: z24.string().nullable(),
|
|
3225
|
+
qualityReportId: z24.string().nullable(),
|
|
3226
|
+
handoffReason: z24.string().nullable(),
|
|
3227
|
+
createdAt: z24.string().datetime(),
|
|
3228
|
+
updatedAt: z24.string().datetime()
|
|
3229
|
+
});
|
|
3230
|
+
var AdminSupportConversation = z24.object({
|
|
3231
|
+
id: z24.string().uuid(),
|
|
3232
|
+
status: AdminSupportConversationStatus,
|
|
3233
|
+
priority: z24.enum([
|
|
3234
|
+
"low",
|
|
3235
|
+
"normal",
|
|
3236
|
+
"high",
|
|
3237
|
+
"urgent"
|
|
3238
|
+
]),
|
|
3239
|
+
currentIntent: z24.string().nullable(),
|
|
3240
|
+
assignedAdminId: z24.string().nullable(),
|
|
3241
|
+
customerServiceWindowEndsAt: z24.string().datetime().nullable(),
|
|
3242
|
+
lastInboundAt: z24.string().datetime().nullable(),
|
|
3243
|
+
createdAt: z24.string().datetime(),
|
|
3244
|
+
updatedAt: z24.string().datetime(),
|
|
3245
|
+
contact: z24.object({
|
|
3246
|
+
id: z24.string().uuid(),
|
|
3247
|
+
userId: z24.string().nullable(),
|
|
3248
|
+
verifiedAt: z24.string().datetime().nullable(),
|
|
3249
|
+
botMode: z24.enum([
|
|
3250
|
+
"enabled",
|
|
3251
|
+
"paused",
|
|
3252
|
+
"opted_out"
|
|
3253
|
+
]),
|
|
3254
|
+
locale: z24.string()
|
|
3255
|
+
}),
|
|
3256
|
+
messages: z24.array(AdminSupportMessage),
|
|
3257
|
+
tickets: z24.array(AdminSupportTicket)
|
|
3258
|
+
});
|
|
3259
|
+
var AdminSupportListQuery = z24.object({
|
|
3260
|
+
status: AdminSupportConversationStatus.optional(),
|
|
3261
|
+
take: z24.coerce.number().int().min(1).max(100).default(50),
|
|
3262
|
+
cursorId: z24.string().uuid().optional()
|
|
3263
|
+
}).strict();
|
|
3264
|
+
var AdminSupportListResponse = z24.object({
|
|
3265
|
+
conversations: z24.array(AdminSupportConversation),
|
|
3266
|
+
nextCursor: z24.string().uuid().nullable()
|
|
3267
|
+
});
|
|
3268
|
+
var AdminSupportReplyRequest = z24.object({
|
|
3269
|
+
body: z24.string().trim().min(1).max(4096),
|
|
3270
|
+
idempotencyKey: z24.string().regex(/^[A-Za-z0-9_-]{8,128}$/)
|
|
3271
|
+
}).strict();
|
|
3272
|
+
var AdminSupportEmptyActionRequest = z24.object({}).strict();
|
|
3273
|
+
var AdminSupportActionResponse = z24.object({
|
|
3274
|
+
conversation: AdminSupportConversation
|
|
3275
|
+
});
|
|
3276
|
+
var AdminSupportReplyResponse = z24.object({
|
|
3277
|
+
messageId: z24.string().uuid(),
|
|
3278
|
+
deliveryStatus: AdminSupportDeliveryStatus,
|
|
3279
|
+
duplicate: z24.boolean()
|
|
3280
|
+
});
|
|
3152
3281
|
|
|
3153
3282
|
// ../../packages/types/src/promo.ts
|
|
3154
3283
|
import { z as z25 } from "zod";
|
|
@@ -3984,6 +4113,23 @@ var DEFAULT_PRICING = {
|
|
|
3984
4113
|
ourCostCents: 0.4,
|
|
3985
4114
|
userPriceCents: 0.8
|
|
3986
4115
|
},
|
|
4116
|
+
// WhatsApp ProductMaker intent proposal. Real OpenRouter usage.cost wins;
|
|
4117
|
+
// this is only the fallback when the relay omits cost.
|
|
4118
|
+
"openrouter:whatsapp-product-agent-intent": {
|
|
4119
|
+
ourCostCents: 0.4,
|
|
4120
|
+
userPriceCents: 0.8
|
|
4121
|
+
},
|
|
4122
|
+
// Internal support calls are cost-only: callers explicitly override user
|
|
4123
|
+
// price to zero. Static COGS estimates apply because OpenAI Responses exposes
|
|
4124
|
+
// token counts but not the provider's dollar charge.
|
|
4125
|
+
"openai:whatsapp-support-classifier": {
|
|
4126
|
+
ourCostCents: 0.1,
|
|
4127
|
+
userPriceCents: 0.2
|
|
4128
|
+
},
|
|
4129
|
+
"openai:whatsapp-support-draft": {
|
|
4130
|
+
ourCostCents: 0.4,
|
|
4131
|
+
userPriceCents: 0.8
|
|
4132
|
+
},
|
|
3987
4133
|
// Task-derived creative recommendation (angle/persona/script/landing-section).
|
|
3988
4134
|
// OpenRouter usage normally supplies the exact COGS and the call layer charges
|
|
3989
4135
|
// 2x. This rule is the fail-closed fallback when usage.cost is unavailable.
|
|
@@ -6930,6 +7076,28 @@ async function submitCreative(api, opts) {
|
|
|
6930
7076
|
});
|
|
6931
7077
|
}
|
|
6932
7078
|
__name(submitCreative, "submitCreative");
|
|
7079
|
+
var ResearchTargetCountryEnum = z38.enum([
|
|
7080
|
+
"CO",
|
|
7081
|
+
"MX",
|
|
7082
|
+
"PE",
|
|
7083
|
+
"EC",
|
|
7084
|
+
"CL"
|
|
7085
|
+
]);
|
|
7086
|
+
var ResearchCategoryEnum = z38.enum([
|
|
7087
|
+
"auto",
|
|
7088
|
+
"automotive",
|
|
7089
|
+
"kitchen",
|
|
7090
|
+
"pets",
|
|
7091
|
+
"home_organization",
|
|
7092
|
+
"beauty",
|
|
7093
|
+
"wellness",
|
|
7094
|
+
"fitness",
|
|
7095
|
+
"baby",
|
|
7096
|
+
"tech",
|
|
7097
|
+
"tools",
|
|
7098
|
+
"garden",
|
|
7099
|
+
"cleaning"
|
|
7100
|
+
]);
|
|
6933
7101
|
|
|
6934
7102
|
// src/errors.ts
|
|
6935
7103
|
var McpErrorCode = {
|
|
@@ -6961,6 +7129,7 @@ var McpErrorCode = {
|
|
|
6961
7129
|
TASK_NOT_READY: "TASK_NOT_READY",
|
|
6962
7130
|
CREATIVE_VARIANT_FAILED: "CREATIVE_VARIANT_FAILED",
|
|
6963
7131
|
CREATIVE_VARIANT_DISPATCH_FAILED: "CREATIVE_VARIANT_DISPATCH_FAILED",
|
|
7132
|
+
PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT",
|
|
6964
7133
|
// Connection presence
|
|
6965
7134
|
NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
|
|
6966
7135
|
NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
|
|
@@ -7082,12 +7251,13 @@ var CODE_MESSAGES = {
|
|
|
7082
7251
|
TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
|
|
7083
7252
|
CREATIVE_VARIANT_FAILED: "The previous creative variant attempt failed. Ask for confirmation before starting a new paid attempt with a new clientRequestId.",
|
|
7084
7253
|
CREATIVE_VARIANT_DISPATCH_FAILED: "The creative variant could not be queued. Retry as a new paid attempt with a new clientRequestId.",
|
|
7254
|
+
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
7255
|
// API key
|
|
7086
7256
|
INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
|
|
7087
7257
|
REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
|
|
7088
7258
|
// Rate
|
|
7089
7259
|
RATE_LIMITED: "Too many requests. Wait a few seconds.",
|
|
7090
|
-
INSUFFICIENT_CREDITS: "Insufficient credits
|
|
7260
|
+
INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
|
|
7091
7261
|
TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
|
|
7092
7262
|
// Generic
|
|
7093
7263
|
UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
|
|
@@ -7100,6 +7270,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
|
|
|
7100
7270
|
McpErrorCode.INVALID_INPUT,
|
|
7101
7271
|
McpErrorCode.INSUFFICIENT_CREDITS,
|
|
7102
7272
|
McpErrorCode.TASK_QUOTA_EXCEEDED,
|
|
7273
|
+
McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
|
|
7103
7274
|
McpErrorCode.INVALID_API_KEY,
|
|
7104
7275
|
McpErrorCode.REVOKED_API_KEY,
|
|
7105
7276
|
McpErrorCode.IMAGE_PATH_NOT_FOUND,
|
|
@@ -7162,6 +7333,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
|
|
|
7162
7333
|
McpErrorCode.INSUFFICIENT_CREDITS,
|
|
7163
7334
|
McpErrorCode.TASK_QUOTA_EXCEEDED,
|
|
7164
7335
|
McpErrorCode.CREATIVE_VARIANT_DISPATCH_FAILED,
|
|
7336
|
+
McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
|
|
7165
7337
|
// Shopify
|
|
7166
7338
|
McpErrorCode.SHOPIFY_NOT_CONNECTED,
|
|
7167
7339
|
McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
|
|
@@ -7275,9 +7447,11 @@ function translateError(err) {
|
|
|
7275
7447
|
const body = isRecord(err.body) ? err.body : {};
|
|
7276
7448
|
if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
|
|
7277
7449
|
const code = body.code;
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7450
|
+
const extra = {};
|
|
7451
|
+
if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
|
|
7452
|
+
if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
|
|
7453
|
+
if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
|
|
7454
|
+
return mcp(code, resolveMessage(code, body.message), extra);
|
|
7281
7455
|
}
|
|
7282
7456
|
if (err.status === 401) {
|
|
7283
7457
|
return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
|
|
@@ -9645,6 +9819,224 @@ function formatPercent(value) {
|
|
|
9645
9819
|
}
|
|
9646
9820
|
__name(formatPercent, "formatPercent");
|
|
9647
9821
|
|
|
9822
|
+
// src/tools/product-research.ts
|
|
9823
|
+
import { z as z49 } from "zod";
|
|
9824
|
+
|
|
9825
|
+
// src/tools/product-research-idempotency.ts
|
|
9826
|
+
import { createHash as createHash2 } from "crypto";
|
|
9827
|
+
var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
|
|
9828
|
+
var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
|
|
9829
|
+
function uuidV5(name, namespace) {
|
|
9830
|
+
const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
|
|
9831
|
+
const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
|
|
9832
|
+
const bytes = Buffer.from(digest.subarray(0, 16));
|
|
9833
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
9834
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
9835
|
+
const hex = bytes.toString("hex");
|
|
9836
|
+
return [
|
|
9837
|
+
hex.slice(0, 8),
|
|
9838
|
+
hex.slice(8, 12),
|
|
9839
|
+
hex.slice(12, 16),
|
|
9840
|
+
hex.slice(16, 20),
|
|
9841
|
+
hex.slice(20)
|
|
9842
|
+
].join("-");
|
|
9843
|
+
}
|
|
9844
|
+
__name(uuidV5, "uuidV5");
|
|
9845
|
+
function deriveIdempotencyKey(input) {
|
|
9846
|
+
const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
|
|
9847
|
+
const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
|
|
9848
|
+
return uuidV5(name, NAMESPACE);
|
|
9849
|
+
}
|
|
9850
|
+
__name(deriveIdempotencyKey, "deriveIdempotencyKey");
|
|
9851
|
+
|
|
9852
|
+
// src/tools/product-research-render.ts
|
|
9853
|
+
var num = /* @__PURE__ */ __name((value, suffix = "") => value === void 0 ? "\u2014" : `${Math.round(value * 100) / 100}${suffix}`, "num");
|
|
9854
|
+
function renderMeta(meta) {
|
|
9855
|
+
if (meta.status === "evidence_failed") {
|
|
9856
|
+
return [
|
|
9857
|
+
"- \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."
|
|
9858
|
+
];
|
|
9859
|
+
}
|
|
9860
|
+
if (meta.status === "evidence_empty") {
|
|
9861
|
+
return [
|
|
9862
|
+
"- \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."
|
|
9863
|
+
];
|
|
9864
|
+
}
|
|
9865
|
+
return [
|
|
9866
|
+
`- **Evidencia en Meta Ads:** ${meta.totalAds} anuncios de ${meta.distinctAdvertisers} anunciantes, m\xE1ximo ${meta.maxActiveDays} d\xEDas activos, oportunidad de \xE1ngulo ${meta.angleOpportunity}/100.`,
|
|
9867
|
+
meta.summary ? ` ${meta.summary}` : ""
|
|
9868
|
+
].filter(Boolean);
|
|
9869
|
+
}
|
|
9870
|
+
__name(renderMeta, "renderMeta");
|
|
9871
|
+
function renderCandidate(candidate, index) {
|
|
9872
|
+
const name = candidate.nameLocal ?? candidate.nameOriginal;
|
|
9873
|
+
const lines = [
|
|
9874
|
+
`### ${index + 1}. ${name}`,
|
|
9875
|
+
`**${candidate.score}/100 \xB7 ${candidate.label} \xB7 confianza ${candidate.confidence}**`
|
|
9876
|
+
];
|
|
9877
|
+
if (candidate.insight) lines.push("", candidate.insight);
|
|
9878
|
+
lines.push("", "**Por qu\xE9:**");
|
|
9879
|
+
lines.push(...candidate.reasons.map((reason) => `- ${reason}`));
|
|
9880
|
+
lines.push("", "**Riesgos:**");
|
|
9881
|
+
lines.push(...candidate.risks.length > 0 ? candidate.risks.map((risk) => `- ${risk}`) : [
|
|
9882
|
+
"- No se identificaron riesgos espec\xEDficos."
|
|
9883
|
+
]);
|
|
9884
|
+
lines.push("", ...renderMeta(candidate.meta));
|
|
9885
|
+
lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
|
|
9886
|
+
if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
|
|
9887
|
+
return lines.join("\n");
|
|
9888
|
+
}
|
|
9889
|
+
__name(renderCandidate, "renderCandidate");
|
|
9890
|
+
function renderResearchReport(run) {
|
|
9891
|
+
if (run.status === "failed" || run.status === "refunded") {
|
|
9892
|
+
return [
|
|
9893
|
+
`\u274C **La investigaci\xF3n no pudo completarse** (estado: ${run.status}).`,
|
|
9894
|
+
run.errorCode ? `C\xF3digo: \`${run.errorCode}\`` : "",
|
|
9895
|
+
run.refunded ? `Los ${run.priceCredits} cr\xE9ditos fueron reembolsados.` : "\u26A0\uFE0F No consta reembolso \u2014 rev\xEDsalo antes de reintentar."
|
|
9896
|
+
].filter(Boolean).join("\n");
|
|
9897
|
+
}
|
|
9898
|
+
if (run.status === "ready" && run.result === null) {
|
|
9899
|
+
return [
|
|
9900
|
+
`\u274C **No se pudo leer el reporte** (estado: ${run.status}).`,
|
|
9901
|
+
`El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3, pero el reporte qued\xF3 corrupto y no puede recuperarse.`,
|
|
9902
|
+
`Contacta a soporte con este runId para revisar un reembolso: \`${run.id}\`.`
|
|
9903
|
+
].join("\n");
|
|
9904
|
+
}
|
|
9905
|
+
if (run.result === null) {
|
|
9906
|
+
return [
|
|
9907
|
+
`\u23F3 **La investigaci\xF3n sigue en curso** (estado: ${run.status}, etapa: ${run.progressStage}).`,
|
|
9908
|
+
`El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3; el reporte no se pierde.`,
|
|
9909
|
+
`Cons\xFAltalo con \`get_product_research_run\` usando runId \`${run.id}\`.`
|
|
9910
|
+
].join("\n");
|
|
9911
|
+
}
|
|
9912
|
+
const { result } = run;
|
|
9913
|
+
const degraded = result.candidates.filter((candidate) => candidate.meta.status !== "evidence_available").length;
|
|
9914
|
+
const header = [
|
|
9915
|
+
`\u2705 **Productos ganadores \xB7 ${result.targetCountry} \xB7 ${result.categoryKey}**`,
|
|
9916
|
+
"",
|
|
9917
|
+
`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.`
|
|
9918
|
+
];
|
|
9919
|
+
if (degraded > 0) {
|
|
9920
|
+
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.`);
|
|
9921
|
+
}
|
|
9922
|
+
return [
|
|
9923
|
+
...header,
|
|
9924
|
+
"",
|
|
9925
|
+
...result.candidates.map((candidate, index) => renderCandidate(candidate, index))
|
|
9926
|
+
].join("\n\n");
|
|
9927
|
+
}
|
|
9928
|
+
__name(renderResearchReport, "renderResearchReport");
|
|
9929
|
+
|
|
9930
|
+
// src/tools/product-research.ts
|
|
9931
|
+
var PRICE_CREDITS = 2e3;
|
|
9932
|
+
var TERMINAL2 = /* @__PURE__ */ new Set([
|
|
9933
|
+
"ready",
|
|
9934
|
+
"failed",
|
|
9935
|
+
"refunded"
|
|
9936
|
+
]);
|
|
9937
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
9938
|
+
var MAX_WAIT_SECONDS = 120;
|
|
9939
|
+
var FindInput = z49.object({
|
|
9940
|
+
targetCountry: ResearchTargetCountryEnum.describe("Pa\xEDs objetivo. S\xF3lo CO, MX, PE, EC y CL tienen cobertura de investigaci\xF3n."),
|
|
9941
|
+
categoryKey: ResearchCategoryEnum.default("auto").describe('Categor\xEDa a investigar. Usa "auto" para que la IA elija seg\xFAn el mercado.'),
|
|
9942
|
+
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.`),
|
|
9943
|
+
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.")
|
|
9944
|
+
});
|
|
9945
|
+
var GetInput = z49.object({
|
|
9946
|
+
runId: z49.string().uuid().describe("ID del run devuelto por find_winning_products.")
|
|
9947
|
+
});
|
|
9948
|
+
var ListInput = z49.object({
|
|
9949
|
+
limit: z49.number().int().min(1).max(20).default(10)
|
|
9950
|
+
});
|
|
9951
|
+
function textResult(text) {
|
|
9952
|
+
return {
|
|
9953
|
+
content: [
|
|
9954
|
+
{
|
|
9955
|
+
type: "text",
|
|
9956
|
+
text
|
|
9957
|
+
}
|
|
9958
|
+
]
|
|
9959
|
+
};
|
|
9960
|
+
}
|
|
9961
|
+
__name(textResult, "textResult");
|
|
9962
|
+
var sleep2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)), "sleep");
|
|
9963
|
+
async function pollUntilTerminal(api, runId, waitSeconds) {
|
|
9964
|
+
const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
|
|
9965
|
+
let run = await api.getJson(`/v1/product-research/runs/${runId}`);
|
|
9966
|
+
while (!TERMINAL2.has(run.status) && Date.now() < deadline) {
|
|
9967
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
9968
|
+
run = await api.getJson(`/v1/product-research/runs/${runId}`);
|
|
9969
|
+
}
|
|
9970
|
+
return run;
|
|
9971
|
+
}
|
|
9972
|
+
__name(pollUntilTerminal, "pollUntilTerminal");
|
|
9973
|
+
function registerProductResearchTools(server, api) {
|
|
9974
|
+
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) => {
|
|
9975
|
+
const parsed = FindInput.parse(input);
|
|
9976
|
+
if (!parsed.confirmed) {
|
|
9977
|
+
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.`);
|
|
9978
|
+
}
|
|
9979
|
+
try {
|
|
9980
|
+
const idempotencyKey = deriveIdempotencyKey({
|
|
9981
|
+
fingerprint: api.fingerprint(),
|
|
9982
|
+
targetCountry: parsed.targetCountry,
|
|
9983
|
+
categoryKey: parsed.categoryKey,
|
|
9984
|
+
nowMs: Date.now()
|
|
9985
|
+
});
|
|
9986
|
+
const created = await api.postJson("/v1/product-research/runs", {
|
|
9987
|
+
targetCountry: parsed.targetCountry,
|
|
9988
|
+
categoryKey: parsed.categoryKey,
|
|
9989
|
+
idempotencyKey
|
|
9990
|
+
});
|
|
9991
|
+
const waitSeconds = parsed.waitSeconds ?? 0;
|
|
9992
|
+
let run = created;
|
|
9993
|
+
if (waitSeconds > 0 && !TERMINAL2.has(created.status)) {
|
|
9994
|
+
try {
|
|
9995
|
+
run = await pollUntilTerminal(api, created.id, waitSeconds);
|
|
9996
|
+
} catch {
|
|
9997
|
+
run = created;
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
const notice = created.reused ? `
|
|
10001
|
+
|
|
10002
|
+
_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._` : "";
|
|
10003
|
+
return textResult(`${renderResearchReport(run)}${notice}`);
|
|
10004
|
+
} catch (error) {
|
|
10005
|
+
throw translateError(error);
|
|
10006
|
+
}
|
|
10007
|
+
});
|
|
10008
|
+
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) => {
|
|
10009
|
+
const parsed = GetInput.parse(input);
|
|
10010
|
+
try {
|
|
10011
|
+
const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
|
|
10012
|
+
return {
|
|
10013
|
+
content: [
|
|
10014
|
+
{
|
|
10015
|
+
type: "text",
|
|
10016
|
+
text: JSON.stringify(run)
|
|
10017
|
+
},
|
|
10018
|
+
{
|
|
10019
|
+
type: "text",
|
|
10020
|
+
text: renderResearchReport(run)
|
|
10021
|
+
}
|
|
10022
|
+
]
|
|
10023
|
+
};
|
|
10024
|
+
} catch (error) {
|
|
10025
|
+
throw translateError(error);
|
|
10026
|
+
}
|
|
10027
|
+
});
|
|
10028
|
+
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) => {
|
|
10029
|
+
const parsed = ListInput.parse(input);
|
|
10030
|
+
try {
|
|
10031
|
+
const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
|
|
10032
|
+
return textResult(JSON.stringify(runs));
|
|
10033
|
+
} catch (error) {
|
|
10034
|
+
throw translateError(error);
|
|
10035
|
+
}
|
|
10036
|
+
});
|
|
10037
|
+
}
|
|
10038
|
+
__name(registerProductResearchTools, "registerProductResearchTools");
|
|
10039
|
+
|
|
9648
10040
|
// src/server.ts
|
|
9649
10041
|
function createServer(opts) {
|
|
9650
10042
|
const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
|
|
@@ -9661,6 +10053,7 @@ function createServer(opts) {
|
|
|
9661
10053
|
});
|
|
9662
10054
|
const api = new ApiClient(apiBaseUrl, apiKey);
|
|
9663
10055
|
registerProductCostingTools(server);
|
|
10056
|
+
registerProductResearchTools(server, api);
|
|
9664
10057
|
registerTaskTools(server, api, {
|
|
9665
10058
|
proxyBaseUrl,
|
|
9666
10059
|
transport
|
package/dist/stdio.js
CHANGED
|
@@ -9,6 +9,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
|
|
11
11
|
// src/api-client.ts
|
|
12
|
+
import { createHash } from "crypto";
|
|
12
13
|
var ApiHttpError = class extends Error {
|
|
13
14
|
static {
|
|
14
15
|
__name(this, "ApiHttpError");
|
|
@@ -35,6 +36,15 @@ var ApiClient = class {
|
|
|
35
36
|
Authorization: `Bearer ${this.apiKey}`
|
|
36
37
|
};
|
|
37
38
|
}
|
|
39
|
+
/**
|
|
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.
|
|
44
|
+
*/
|
|
45
|
+
fingerprint() {
|
|
46
|
+
return createHash("sha256").update(this.apiKey).digest("hex").slice(0, 32);
|
|
47
|
+
}
|
|
38
48
|
async getJson(path3) {
|
|
39
49
|
const res = await fetch(`${this.baseUrl}${path3}`, {
|
|
40
50
|
headers: this.authHeaders()
|
|
@@ -129,7 +139,7 @@ var ApiClient = class {
|
|
|
129
139
|
};
|
|
130
140
|
|
|
131
141
|
// src/index.ts
|
|
132
|
-
var MCP_VERSION = "1.
|
|
142
|
+
var MCP_VERSION = "1.17.0";
|
|
133
143
|
|
|
134
144
|
// src/tools/tasks.ts
|
|
135
145
|
import { z as z39 } from "zod";
|
|
@@ -3146,6 +3156,125 @@ var AdminQualityReportListResponse = z24.object({
|
|
|
3146
3156
|
var UpdateQualityReportStatusRequest = z24.object({
|
|
3147
3157
|
status: QualityReportStatus
|
|
3148
3158
|
});
|
|
3159
|
+
var AdminSupportConversationStatus = z24.enum([
|
|
3160
|
+
"bot_active",
|
|
3161
|
+
"human_pending",
|
|
3162
|
+
"human_active",
|
|
3163
|
+
"closed"
|
|
3164
|
+
]);
|
|
3165
|
+
var AdminSupportDeliveryStatus = z24.enum([
|
|
3166
|
+
"draft",
|
|
3167
|
+
"pending",
|
|
3168
|
+
"sent",
|
|
3169
|
+
"delivered",
|
|
3170
|
+
"read",
|
|
3171
|
+
"failed",
|
|
3172
|
+
"blocked"
|
|
3173
|
+
]);
|
|
3174
|
+
var AdminSupportMessage = z24.object({
|
|
3175
|
+
id: z24.string().uuid(),
|
|
3176
|
+
direction: z24.enum([
|
|
3177
|
+
"inbound",
|
|
3178
|
+
"outbound"
|
|
3179
|
+
]),
|
|
3180
|
+
senderKind: z24.enum([
|
|
3181
|
+
"user",
|
|
3182
|
+
"bot",
|
|
3183
|
+
"admin",
|
|
3184
|
+
"system"
|
|
3185
|
+
]),
|
|
3186
|
+
messageType: z24.enum([
|
|
3187
|
+
"text",
|
|
3188
|
+
"image",
|
|
3189
|
+
"audio",
|
|
3190
|
+
"document",
|
|
3191
|
+
"interactive",
|
|
3192
|
+
"status"
|
|
3193
|
+
]),
|
|
3194
|
+
bodyRedacted: z24.string().nullable(),
|
|
3195
|
+
deliveryStatus: AdminSupportDeliveryStatus.nullable(),
|
|
3196
|
+
createdAt: z24.string().datetime()
|
|
3197
|
+
});
|
|
3198
|
+
var AdminSupportTicket = z24.object({
|
|
3199
|
+
id: z24.string().uuid(),
|
|
3200
|
+
category: z24.enum([
|
|
3201
|
+
"how_to",
|
|
3202
|
+
"credits",
|
|
3203
|
+
"billing",
|
|
3204
|
+
"task_status",
|
|
3205
|
+
"task_error",
|
|
3206
|
+
"video_quality",
|
|
3207
|
+
"landing_quality",
|
|
3208
|
+
"image_quality",
|
|
3209
|
+
"feature_request",
|
|
3210
|
+
"other"
|
|
3211
|
+
]),
|
|
3212
|
+
failureMode: z24.string().nullable(),
|
|
3213
|
+
status: z24.enum([
|
|
3214
|
+
"open",
|
|
3215
|
+
"triaged",
|
|
3216
|
+
"resolved",
|
|
3217
|
+
"rejected"
|
|
3218
|
+
]),
|
|
3219
|
+
summary: z24.string(),
|
|
3220
|
+
taskId: z24.string().nullable(),
|
|
3221
|
+
mixTaskId: z24.string().nullable(),
|
|
3222
|
+
qualityReportId: z24.string().nullable(),
|
|
3223
|
+
handoffReason: z24.string().nullable(),
|
|
3224
|
+
createdAt: z24.string().datetime(),
|
|
3225
|
+
updatedAt: z24.string().datetime()
|
|
3226
|
+
});
|
|
3227
|
+
var AdminSupportConversation = z24.object({
|
|
3228
|
+
id: z24.string().uuid(),
|
|
3229
|
+
status: AdminSupportConversationStatus,
|
|
3230
|
+
priority: z24.enum([
|
|
3231
|
+
"low",
|
|
3232
|
+
"normal",
|
|
3233
|
+
"high",
|
|
3234
|
+
"urgent"
|
|
3235
|
+
]),
|
|
3236
|
+
currentIntent: z24.string().nullable(),
|
|
3237
|
+
assignedAdminId: z24.string().nullable(),
|
|
3238
|
+
customerServiceWindowEndsAt: z24.string().datetime().nullable(),
|
|
3239
|
+
lastInboundAt: z24.string().datetime().nullable(),
|
|
3240
|
+
createdAt: z24.string().datetime(),
|
|
3241
|
+
updatedAt: z24.string().datetime(),
|
|
3242
|
+
contact: z24.object({
|
|
3243
|
+
id: z24.string().uuid(),
|
|
3244
|
+
userId: z24.string().nullable(),
|
|
3245
|
+
verifiedAt: z24.string().datetime().nullable(),
|
|
3246
|
+
botMode: z24.enum([
|
|
3247
|
+
"enabled",
|
|
3248
|
+
"paused",
|
|
3249
|
+
"opted_out"
|
|
3250
|
+
]),
|
|
3251
|
+
locale: z24.string()
|
|
3252
|
+
}),
|
|
3253
|
+
messages: z24.array(AdminSupportMessage),
|
|
3254
|
+
tickets: z24.array(AdminSupportTicket)
|
|
3255
|
+
});
|
|
3256
|
+
var AdminSupportListQuery = z24.object({
|
|
3257
|
+
status: AdminSupportConversationStatus.optional(),
|
|
3258
|
+
take: z24.coerce.number().int().min(1).max(100).default(50),
|
|
3259
|
+
cursorId: z24.string().uuid().optional()
|
|
3260
|
+
}).strict();
|
|
3261
|
+
var AdminSupportListResponse = z24.object({
|
|
3262
|
+
conversations: z24.array(AdminSupportConversation),
|
|
3263
|
+
nextCursor: z24.string().uuid().nullable()
|
|
3264
|
+
});
|
|
3265
|
+
var AdminSupportReplyRequest = z24.object({
|
|
3266
|
+
body: z24.string().trim().min(1).max(4096),
|
|
3267
|
+
idempotencyKey: z24.string().regex(/^[A-Za-z0-9_-]{8,128}$/)
|
|
3268
|
+
}).strict();
|
|
3269
|
+
var AdminSupportEmptyActionRequest = z24.object({}).strict();
|
|
3270
|
+
var AdminSupportActionResponse = z24.object({
|
|
3271
|
+
conversation: AdminSupportConversation
|
|
3272
|
+
});
|
|
3273
|
+
var AdminSupportReplyResponse = z24.object({
|
|
3274
|
+
messageId: z24.string().uuid(),
|
|
3275
|
+
deliveryStatus: AdminSupportDeliveryStatus,
|
|
3276
|
+
duplicate: z24.boolean()
|
|
3277
|
+
});
|
|
3149
3278
|
|
|
3150
3279
|
// ../../packages/types/src/promo.ts
|
|
3151
3280
|
import { z as z25 } from "zod";
|
|
@@ -3981,6 +4110,23 @@ var DEFAULT_PRICING = {
|
|
|
3981
4110
|
ourCostCents: 0.4,
|
|
3982
4111
|
userPriceCents: 0.8
|
|
3983
4112
|
},
|
|
4113
|
+
// WhatsApp ProductMaker intent proposal. Real OpenRouter usage.cost wins;
|
|
4114
|
+
// this is only the fallback when the relay omits cost.
|
|
4115
|
+
"openrouter:whatsapp-product-agent-intent": {
|
|
4116
|
+
ourCostCents: 0.4,
|
|
4117
|
+
userPriceCents: 0.8
|
|
4118
|
+
},
|
|
4119
|
+
// Internal support calls are cost-only: callers explicitly override user
|
|
4120
|
+
// price to zero. Static COGS estimates apply because OpenAI Responses exposes
|
|
4121
|
+
// token counts but not the provider's dollar charge.
|
|
4122
|
+
"openai:whatsapp-support-classifier": {
|
|
4123
|
+
ourCostCents: 0.1,
|
|
4124
|
+
userPriceCents: 0.2
|
|
4125
|
+
},
|
|
4126
|
+
"openai:whatsapp-support-draft": {
|
|
4127
|
+
ourCostCents: 0.4,
|
|
4128
|
+
userPriceCents: 0.8
|
|
4129
|
+
},
|
|
3984
4130
|
// Task-derived creative recommendation (angle/persona/script/landing-section).
|
|
3985
4131
|
// OpenRouter usage normally supplies the exact COGS and the call layer charges
|
|
3986
4132
|
// 2x. This rule is the fail-closed fallback when usage.cost is unavailable.
|
|
@@ -6927,6 +7073,28 @@ async function submitCreative(api, opts) {
|
|
|
6927
7073
|
});
|
|
6928
7074
|
}
|
|
6929
7075
|
__name(submitCreative, "submitCreative");
|
|
7076
|
+
var ResearchTargetCountryEnum = z38.enum([
|
|
7077
|
+
"CO",
|
|
7078
|
+
"MX",
|
|
7079
|
+
"PE",
|
|
7080
|
+
"EC",
|
|
7081
|
+
"CL"
|
|
7082
|
+
]);
|
|
7083
|
+
var ResearchCategoryEnum = z38.enum([
|
|
7084
|
+
"auto",
|
|
7085
|
+
"automotive",
|
|
7086
|
+
"kitchen",
|
|
7087
|
+
"pets",
|
|
7088
|
+
"home_organization",
|
|
7089
|
+
"beauty",
|
|
7090
|
+
"wellness",
|
|
7091
|
+
"fitness",
|
|
7092
|
+
"baby",
|
|
7093
|
+
"tech",
|
|
7094
|
+
"tools",
|
|
7095
|
+
"garden",
|
|
7096
|
+
"cleaning"
|
|
7097
|
+
]);
|
|
6930
7098
|
|
|
6931
7099
|
// src/errors.ts
|
|
6932
7100
|
var McpErrorCode = {
|
|
@@ -6958,6 +7126,7 @@ var McpErrorCode = {
|
|
|
6958
7126
|
TASK_NOT_READY: "TASK_NOT_READY",
|
|
6959
7127
|
CREATIVE_VARIANT_FAILED: "CREATIVE_VARIANT_FAILED",
|
|
6960
7128
|
CREATIVE_VARIANT_DISPATCH_FAILED: "CREATIVE_VARIANT_DISPATCH_FAILED",
|
|
7129
|
+
PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT: "PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT",
|
|
6961
7130
|
// Connection presence
|
|
6962
7131
|
NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
|
|
6963
7132
|
NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
|
|
@@ -7079,12 +7248,13 @@ var CODE_MESSAGES = {
|
|
|
7079
7248
|
TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
|
|
7080
7249
|
CREATIVE_VARIANT_FAILED: "The previous creative variant attempt failed. Ask for confirmation before starting a new paid attempt with a new clientRequestId.",
|
|
7081
7250
|
CREATIVE_VARIANT_DISPATCH_FAILED: "The creative variant could not be queued. Retry as a new paid attempt with a new clientRequestId.",
|
|
7251
|
+
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.",
|
|
7082
7252
|
// API key
|
|
7083
7253
|
INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
|
|
7084
7254
|
REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
|
|
7085
7255
|
// Rate
|
|
7086
7256
|
RATE_LIMITED: "Too many requests. Wait a few seconds.",
|
|
7087
|
-
INSUFFICIENT_CREDITS: "Insufficient credits
|
|
7257
|
+
INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
|
|
7088
7258
|
TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
|
|
7089
7259
|
// Generic
|
|
7090
7260
|
UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
|
|
@@ -7097,6 +7267,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
|
|
|
7097
7267
|
McpErrorCode.INVALID_INPUT,
|
|
7098
7268
|
McpErrorCode.INSUFFICIENT_CREDITS,
|
|
7099
7269
|
McpErrorCode.TASK_QUOTA_EXCEEDED,
|
|
7270
|
+
McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
|
|
7100
7271
|
McpErrorCode.INVALID_API_KEY,
|
|
7101
7272
|
McpErrorCode.REVOKED_API_KEY,
|
|
7102
7273
|
McpErrorCode.IMAGE_PATH_NOT_FOUND,
|
|
@@ -7159,6 +7330,7 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
|
|
|
7159
7330
|
McpErrorCode.INSUFFICIENT_CREDITS,
|
|
7160
7331
|
McpErrorCode.TASK_QUOTA_EXCEEDED,
|
|
7161
7332
|
McpErrorCode.CREATIVE_VARIANT_DISPATCH_FAILED,
|
|
7333
|
+
McpErrorCode.PRODUCT_RESEARCH_IDEMPOTENCY_CONFLICT,
|
|
7162
7334
|
// Shopify
|
|
7163
7335
|
McpErrorCode.SHOPIFY_NOT_CONNECTED,
|
|
7164
7336
|
McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
|
|
@@ -7272,9 +7444,11 @@ function translateError(err) {
|
|
|
7272
7444
|
const body = isRecord(err.body) ? err.body : {};
|
|
7273
7445
|
if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
|
|
7274
7446
|
const code = body.code;
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7447
|
+
const extra = {};
|
|
7448
|
+
if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
|
|
7449
|
+
if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
|
|
7450
|
+
if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
|
|
7451
|
+
return mcp(code, resolveMessage(code, body.message), extra);
|
|
7278
7452
|
}
|
|
7279
7453
|
if (err.status === 401) {
|
|
7280
7454
|
return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
|
|
@@ -9601,6 +9775,224 @@ function formatPercent(value) {
|
|
|
9601
9775
|
}
|
|
9602
9776
|
__name(formatPercent, "formatPercent");
|
|
9603
9777
|
|
|
9778
|
+
// src/tools/product-research.ts
|
|
9779
|
+
import { z as z49 } from "zod";
|
|
9780
|
+
|
|
9781
|
+
// src/tools/product-research-idempotency.ts
|
|
9782
|
+
import { createHash as createHash2 } from "crypto";
|
|
9783
|
+
var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
|
|
9784
|
+
var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
|
|
9785
|
+
function uuidV5(name, namespace) {
|
|
9786
|
+
const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
|
|
9787
|
+
const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
|
|
9788
|
+
const bytes = Buffer.from(digest.subarray(0, 16));
|
|
9789
|
+
bytes[6] = (bytes[6] ?? 0) & 15 | 80;
|
|
9790
|
+
bytes[8] = (bytes[8] ?? 0) & 63 | 128;
|
|
9791
|
+
const hex = bytes.toString("hex");
|
|
9792
|
+
return [
|
|
9793
|
+
hex.slice(0, 8),
|
|
9794
|
+
hex.slice(8, 12),
|
|
9795
|
+
hex.slice(12, 16),
|
|
9796
|
+
hex.slice(16, 20),
|
|
9797
|
+
hex.slice(20)
|
|
9798
|
+
].join("-");
|
|
9799
|
+
}
|
|
9800
|
+
__name(uuidV5, "uuidV5");
|
|
9801
|
+
function deriveIdempotencyKey(input) {
|
|
9802
|
+
const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
|
|
9803
|
+
const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
|
|
9804
|
+
return uuidV5(name, NAMESPACE);
|
|
9805
|
+
}
|
|
9806
|
+
__name(deriveIdempotencyKey, "deriveIdempotencyKey");
|
|
9807
|
+
|
|
9808
|
+
// src/tools/product-research-render.ts
|
|
9809
|
+
var num = /* @__PURE__ */ __name((value, suffix = "") => value === void 0 ? "\u2014" : `${Math.round(value * 100) / 100}${suffix}`, "num");
|
|
9810
|
+
function renderMeta(meta) {
|
|
9811
|
+
if (meta.status === "evidence_failed") {
|
|
9812
|
+
return [
|
|
9813
|
+
"- \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."
|
|
9814
|
+
];
|
|
9815
|
+
}
|
|
9816
|
+
if (meta.status === "evidence_empty") {
|
|
9817
|
+
return [
|
|
9818
|
+
"- \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."
|
|
9819
|
+
];
|
|
9820
|
+
}
|
|
9821
|
+
return [
|
|
9822
|
+
`- **Evidencia en Meta Ads:** ${meta.totalAds} anuncios de ${meta.distinctAdvertisers} anunciantes, m\xE1ximo ${meta.maxActiveDays} d\xEDas activos, oportunidad de \xE1ngulo ${meta.angleOpportunity}/100.`,
|
|
9823
|
+
meta.summary ? ` ${meta.summary}` : ""
|
|
9824
|
+
].filter(Boolean);
|
|
9825
|
+
}
|
|
9826
|
+
__name(renderMeta, "renderMeta");
|
|
9827
|
+
function renderCandidate(candidate, index) {
|
|
9828
|
+
const name = candidate.nameLocal ?? candidate.nameOriginal;
|
|
9829
|
+
const lines = [
|
|
9830
|
+
`### ${index + 1}. ${name}`,
|
|
9831
|
+
`**${candidate.score}/100 \xB7 ${candidate.label} \xB7 confianza ${candidate.confidence}**`
|
|
9832
|
+
];
|
|
9833
|
+
if (candidate.insight) lines.push("", candidate.insight);
|
|
9834
|
+
lines.push("", "**Por qu\xE9:**");
|
|
9835
|
+
lines.push(...candidate.reasons.map((reason) => `- ${reason}`));
|
|
9836
|
+
lines.push("", "**Riesgos:**");
|
|
9837
|
+
lines.push(...candidate.risks.length > 0 ? candidate.risks.map((risk) => `- ${risk}`) : [
|
|
9838
|
+
"- No se identificaron riesgos espec\xEDficos."
|
|
9839
|
+
]);
|
|
9840
|
+
lines.push("", ...renderMeta(candidate.meta));
|
|
9841
|
+
lines.push("", `- **Venta (30d):** ${num(candidate.units30d)} unidades \xB7 ${num(candidate.revenueUsd30d, " USD")} \xB7 crecimiento ${num(candidate.growthPct30d, "%")} \xB7 precio ${num(candidate.priceUsd, " USD")}`);
|
|
9842
|
+
if (candidate.productUrl) lines.push(`- [Ver producto](${candidate.productUrl})`);
|
|
9843
|
+
return lines.join("\n");
|
|
9844
|
+
}
|
|
9845
|
+
__name(renderCandidate, "renderCandidate");
|
|
9846
|
+
function renderResearchReport(run) {
|
|
9847
|
+
if (run.status === "failed" || run.status === "refunded") {
|
|
9848
|
+
return [
|
|
9849
|
+
`\u274C **La investigaci\xF3n no pudo completarse** (estado: ${run.status}).`,
|
|
9850
|
+
run.errorCode ? `C\xF3digo: \`${run.errorCode}\`` : "",
|
|
9851
|
+
run.refunded ? `Los ${run.priceCredits} cr\xE9ditos fueron reembolsados.` : "\u26A0\uFE0F No consta reembolso \u2014 rev\xEDsalo antes de reintentar."
|
|
9852
|
+
].filter(Boolean).join("\n");
|
|
9853
|
+
}
|
|
9854
|
+
if (run.status === "ready" && run.result === null) {
|
|
9855
|
+
return [
|
|
9856
|
+
`\u274C **No se pudo leer el reporte** (estado: ${run.status}).`,
|
|
9857
|
+
`El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3, pero el reporte qued\xF3 corrupto y no puede recuperarse.`,
|
|
9858
|
+
`Contacta a soporte con este runId para revisar un reembolso: \`${run.id}\`.`
|
|
9859
|
+
].join("\n");
|
|
9860
|
+
}
|
|
9861
|
+
if (run.result === null) {
|
|
9862
|
+
return [
|
|
9863
|
+
`\u23F3 **La investigaci\xF3n sigue en curso** (estado: ${run.status}, etapa: ${run.progressStage}).`,
|
|
9864
|
+
`El cargo de ${run.priceCredits} cr\xE9ditos ya se realiz\xF3; el reporte no se pierde.`,
|
|
9865
|
+
`Cons\xFAltalo con \`get_product_research_run\` usando runId \`${run.id}\`.`
|
|
9866
|
+
].join("\n");
|
|
9867
|
+
}
|
|
9868
|
+
const { result } = run;
|
|
9869
|
+
const degraded = result.candidates.filter((candidate) => candidate.meta.status !== "evidence_available").length;
|
|
9870
|
+
const header = [
|
|
9871
|
+
`\u2705 **Productos ganadores \xB7 ${result.targetCountry} \xB7 ${result.categoryKey}**`,
|
|
9872
|
+
"",
|
|
9873
|
+
`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.`
|
|
9874
|
+
];
|
|
9875
|
+
if (degraded > 0) {
|
|
9876
|
+
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.`);
|
|
9877
|
+
}
|
|
9878
|
+
return [
|
|
9879
|
+
...header,
|
|
9880
|
+
"",
|
|
9881
|
+
...result.candidates.map((candidate, index) => renderCandidate(candidate, index))
|
|
9882
|
+
].join("\n\n");
|
|
9883
|
+
}
|
|
9884
|
+
__name(renderResearchReport, "renderResearchReport");
|
|
9885
|
+
|
|
9886
|
+
// src/tools/product-research.ts
|
|
9887
|
+
var PRICE_CREDITS = 2e3;
|
|
9888
|
+
var TERMINAL2 = /* @__PURE__ */ new Set([
|
|
9889
|
+
"ready",
|
|
9890
|
+
"failed",
|
|
9891
|
+
"refunded"
|
|
9892
|
+
]);
|
|
9893
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
9894
|
+
var MAX_WAIT_SECONDS = 120;
|
|
9895
|
+
var FindInput = z49.object({
|
|
9896
|
+
targetCountry: ResearchTargetCountryEnum.describe("Pa\xEDs objetivo. S\xF3lo CO, MX, PE, EC y CL tienen cobertura de investigaci\xF3n."),
|
|
9897
|
+
categoryKey: ResearchCategoryEnum.default("auto").describe('Categor\xEDa a investigar. Usa "auto" para que la IA elija seg\xFAn el mercado.'),
|
|
9898
|
+
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.`),
|
|
9899
|
+
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.")
|
|
9900
|
+
});
|
|
9901
|
+
var GetInput = z49.object({
|
|
9902
|
+
runId: z49.string().uuid().describe("ID del run devuelto por find_winning_products.")
|
|
9903
|
+
});
|
|
9904
|
+
var ListInput = z49.object({
|
|
9905
|
+
limit: z49.number().int().min(1).max(20).default(10)
|
|
9906
|
+
});
|
|
9907
|
+
function textResult(text) {
|
|
9908
|
+
return {
|
|
9909
|
+
content: [
|
|
9910
|
+
{
|
|
9911
|
+
type: "text",
|
|
9912
|
+
text
|
|
9913
|
+
}
|
|
9914
|
+
]
|
|
9915
|
+
};
|
|
9916
|
+
}
|
|
9917
|
+
__name(textResult, "textResult");
|
|
9918
|
+
var sleep2 = /* @__PURE__ */ __name((ms) => new Promise((resolve3) => setTimeout(resolve3, ms)), "sleep");
|
|
9919
|
+
async function pollUntilTerminal(api, runId, waitSeconds) {
|
|
9920
|
+
const deadline = Date.now() + Math.min(waitSeconds, MAX_WAIT_SECONDS) * 1e3;
|
|
9921
|
+
let run = await api.getJson(`/v1/product-research/runs/${runId}`);
|
|
9922
|
+
while (!TERMINAL2.has(run.status) && Date.now() < deadline) {
|
|
9923
|
+
await sleep2(POLL_INTERVAL_MS);
|
|
9924
|
+
run = await api.getJson(`/v1/product-research/runs/${runId}`);
|
|
9925
|
+
}
|
|
9926
|
+
return run;
|
|
9927
|
+
}
|
|
9928
|
+
__name(pollUntilTerminal, "pollUntilTerminal");
|
|
9929
|
+
function registerProductResearchTools(server, api) {
|
|
9930
|
+
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) => {
|
|
9931
|
+
const parsed = FindInput.parse(input);
|
|
9932
|
+
if (!parsed.confirmed) {
|
|
9933
|
+
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.`);
|
|
9934
|
+
}
|
|
9935
|
+
try {
|
|
9936
|
+
const idempotencyKey = deriveIdempotencyKey({
|
|
9937
|
+
fingerprint: api.fingerprint(),
|
|
9938
|
+
targetCountry: parsed.targetCountry,
|
|
9939
|
+
categoryKey: parsed.categoryKey,
|
|
9940
|
+
nowMs: Date.now()
|
|
9941
|
+
});
|
|
9942
|
+
const created = await api.postJson("/v1/product-research/runs", {
|
|
9943
|
+
targetCountry: parsed.targetCountry,
|
|
9944
|
+
categoryKey: parsed.categoryKey,
|
|
9945
|
+
idempotencyKey
|
|
9946
|
+
});
|
|
9947
|
+
const waitSeconds = parsed.waitSeconds ?? 0;
|
|
9948
|
+
let run = created;
|
|
9949
|
+
if (waitSeconds > 0 && !TERMINAL2.has(created.status)) {
|
|
9950
|
+
try {
|
|
9951
|
+
run = await pollUntilTerminal(api, created.id, waitSeconds);
|
|
9952
|
+
} catch {
|
|
9953
|
+
run = created;
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
const notice = created.reused ? `
|
|
9957
|
+
|
|
9958
|
+
_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._` : "";
|
|
9959
|
+
return textResult(`${renderResearchReport(run)}${notice}`);
|
|
9960
|
+
} catch (error) {
|
|
9961
|
+
throw translateError(error);
|
|
9962
|
+
}
|
|
9963
|
+
});
|
|
9964
|
+
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) => {
|
|
9965
|
+
const parsed = GetInput.parse(input);
|
|
9966
|
+
try {
|
|
9967
|
+
const run = await api.getJson(`/v1/product-research/runs/${parsed.runId}`);
|
|
9968
|
+
return {
|
|
9969
|
+
content: [
|
|
9970
|
+
{
|
|
9971
|
+
type: "text",
|
|
9972
|
+
text: JSON.stringify(run)
|
|
9973
|
+
},
|
|
9974
|
+
{
|
|
9975
|
+
type: "text",
|
|
9976
|
+
text: renderResearchReport(run)
|
|
9977
|
+
}
|
|
9978
|
+
]
|
|
9979
|
+
};
|
|
9980
|
+
} catch (error) {
|
|
9981
|
+
throw translateError(error);
|
|
9982
|
+
}
|
|
9983
|
+
});
|
|
9984
|
+
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) => {
|
|
9985
|
+
const parsed = ListInput.parse(input);
|
|
9986
|
+
try {
|
|
9987
|
+
const runs = await api.getJson(`/v1/product-research/runs?limit=${parsed.limit}`);
|
|
9988
|
+
return textResult(JSON.stringify(runs));
|
|
9989
|
+
} catch (error) {
|
|
9990
|
+
throw translateError(error);
|
|
9991
|
+
}
|
|
9992
|
+
});
|
|
9993
|
+
}
|
|
9994
|
+
__name(registerProductResearchTools, "registerProductResearchTools");
|
|
9995
|
+
|
|
9604
9996
|
// src/server.ts
|
|
9605
9997
|
function createServer(opts) {
|
|
9606
9998
|
const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
|
|
@@ -9617,6 +10009,7 @@ function createServer(opts) {
|
|
|
9617
10009
|
});
|
|
9618
10010
|
const api = new ApiClient(apiBaseUrl, apiKey);
|
|
9619
10011
|
registerProductCostingTools(server);
|
|
10012
|
+
registerProductResearchTools(server, api);
|
|
9620
10013
|
registerTaskTools(server, api, {
|
|
9621
10014
|
proxyBaseUrl,
|
|
9622
10015
|
transport
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
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.",
|
|
@@ -63,8 +63,9 @@
|
|
|
63
63
|
"tsx": "^4.16.0",
|
|
64
64
|
"typescript": "^7.0.2",
|
|
65
65
|
"vitest": "^2.1.9",
|
|
66
|
-
"@packages/
|
|
67
|
-
"@packages/product-
|
|
66
|
+
"@packages/product-economics": "0.1.0",
|
|
67
|
+
"@packages/product-research": "0.1.0",
|
|
68
|
+
"@packages/types": "0.1.0"
|
|
68
69
|
},
|
|
69
70
|
"publishConfig": {
|
|
70
71
|
"access": "public"
|