@nooticr/mcp 1.26.36 → 1.26.38
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/shared/amazon.js +25 -7
- package/dist/shared/loading-plans.js +42 -0
- package/dist/shared/marketplace.js +374 -0
- package/dist/shared/output-schemas.js +21 -0
- package/dist/shared/tools-def.js +18 -0
- package/dist/shared/tools.js +13 -1
- package/dist/shared/ui-template.js +70 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,6 +145,9 @@ a `scanId` for the rest.
|
|
|
145
145
|
| `scan_amazon_category` | 3 per listing collected (30 by default) | The category, collected: listings with prices, star histograms, Amazon's own review-aspect counts and the review text itself, plus the arithmetic over them — price spread, rating spread, which aspects recur across how many brands, and the share of each listing's ratings sitting at 1-2 stars, read from the histogram rather than from the handful of reviews a scrape returns. Pass `query` to search the category, `asins` to pin the competitors a customer named, or both; named competitors keep their place at the front. Because the price is set by an argument, it confirms the total before spending it. |
|
|
146
146
|
| `amazon_scan_status` | free | Picks a running collection back up by `scanId` — the listings collected since, with the same rollup. Free: it is the poll `scan_amazon_category` asked for, and billing the second half of one answer would charge for a wait the server chose. |
|
|
147
147
|
| `get_amazon_product` | 3 | One listing by ASIN or URL: price, rating, the full star histogram, features, specs, Amazon's review digest and aspect breakdown, and the reviews. |
|
|
148
|
+
| `scan_marketplace_category` | 3 per product collected (30 by default) | The same category read on ten more sites — AliExpress, Cdiscount, Flipkart, Mercado Libre, Lazada, Otto, Rakuten, Trendyol, Jumia and Temu. `marketplace` picks the site; `market` picks the storefront for the sites that have several, and the argument names the real set per site so a model cannot send Otto a Lazada region. Same rollup, same confirmation before spending. |
|
|
149
|
+
| `marketplace_scan_status` | free | Picks a running marketplace collection back up by `scanId` and the same `marketplace`. Free, for the same reason `amazon_scan_status` is. |
|
|
150
|
+
| `get_marketplace_product` | 3 | One product from any of the eleven sites, by id or URL: price, rating, the full star histogram, specs and the reviews. |
|
|
148
151
|
| `show_amazon_category_insights` | free | Draws the read **your model** wrote — purchase drivers, barriers, what each brand does well, the gaps, the positioning angles — beside the listings and their reviews, so a person can click a product and check any claim against the text it came from. The conclusions are attributed to your model, never presented as a nooticr rating of anyone's product. Makes no requests. |
|
|
149
152
|
|
|
150
153
|
### Make something
|
package/dist/shared/amazon.js
CHANGED
|
@@ -29,11 +29,24 @@ export function normaliseProducts(raw) {
|
|
|
29
29
|
return [];
|
|
30
30
|
return raw.map((entry) => {
|
|
31
31
|
const p = (entry ?? {});
|
|
32
|
+
// Resolved once: the reviews below are addressed by it too, and reading the
|
|
33
|
+
// raw `p.asin` there gave every review on every non-Amazon product the id
|
|
34
|
+
// "review::0", "review::1" — colliding across products, so a model citing
|
|
35
|
+
// one could not be pointed back at the right listing.
|
|
36
|
+
const id = String(p.asin ?? p.id ?? p.item_id ?? p.sku ?? p.product_id ?? "");
|
|
32
37
|
const histogram = (p.rating_histogram ?? {});
|
|
33
38
|
const reviews = Array.isArray(p.reviews) ? p.reviews : [];
|
|
34
39
|
const aspects = Array.isArray(p.review_aspects) ? p.review_aspects : [];
|
|
35
40
|
return {
|
|
36
|
-
|
|
41
|
+
// The id this product is addressed by, whatever the site calls it.
|
|
42
|
+
//
|
|
43
|
+
// The field is named `asin` because Amazon was first and the view keys
|
|
44
|
+
// tiles, the review drill-down and the `perProduct` match on it. Left at
|
|
45
|
+
// "" for the other ten sites — which is what it was — every product in a
|
|
46
|
+
// Lazada or Otto scan shared one blank key: the tiles rendered, and not
|
|
47
|
+
// one of them could open its reviews, because the click looked up a
|
|
48
|
+
// product by an id every product had.
|
|
49
|
+
asin: id,
|
|
37
50
|
url: String(p.url ?? (p.asin ? `https://www.amazon.com/dp/${p.asin}` : "")),
|
|
38
51
|
title: String(p.title ?? ""),
|
|
39
52
|
brand: String(p.brand ?? ""),
|
|
@@ -68,8 +81,8 @@ export function normaliseProducts(raw) {
|
|
|
68
81
|
// Addressable, so the model's analysis can point back at the exact
|
|
69
82
|
// review it read a driver or a barrier out of. Same shape
|
|
70
83
|
// analyze_comments mints for a comment.
|
|
71
|
-
id: String(row.id ?? `review:${
|
|
72
|
-
asin:
|
|
84
|
+
id: String(row.id ?? `review:${id}:${i}`),
|
|
85
|
+
asin: id,
|
|
73
86
|
brand: String(p.brand ?? ""),
|
|
74
87
|
author: String(row.author ?? ""),
|
|
75
88
|
rating: num(row.rating),
|
|
@@ -104,8 +117,11 @@ export function allReviews(products) {
|
|
|
104
117
|
* rule in CLAUDE.md.
|
|
105
118
|
*/
|
|
106
119
|
export function categoryGuidance(opts) {
|
|
120
|
+
const site = opts.site ?? "Amazon";
|
|
121
|
+
const statusTool = opts.statusTool ?? "amazon_scan_status";
|
|
122
|
+
const insightsTool = opts.insightsTool === undefined ? "show_amazon_category_insights" : opts.insightsTool;
|
|
107
123
|
const lines = [];
|
|
108
|
-
lines.push(`Here are ${opts.products}
|
|
124
|
+
lines.push(`Here are ${opts.products} ${site} listing${opts.products === 1 ? "" : "s"} for ${opts.label}` +
|
|
109
125
|
`, carrying ${opts.reviews} review${opts.reviews === 1 ? "" : "s"}` +
|
|
110
126
|
(opts.ratingsRepresented
|
|
111
127
|
? ` and the star histograms behind ${opts.ratingsRepresented.toLocaleString("en-US")} ratings.`
|
|
@@ -114,7 +130,7 @@ export function categoryGuidance(opts) {
|
|
|
114
130
|
lines.push(`Brands in the set: ${opts.brands.join(", ")}.`);
|
|
115
131
|
if (!opts.complete) {
|
|
116
132
|
lines.push(`Collection is still running — ${opts.pending} listing${opts.pending === 1 ? "" : "s"} to go. ` +
|
|
117
|
-
`Everything below is real and usable now; call
|
|
133
|
+
`Everything below is real and usable now; call ${statusTool} with scanId "${opts.scanId}" ` +
|
|
118
134
|
`for the rest before you write a final read.`);
|
|
119
135
|
}
|
|
120
136
|
lines.push("");
|
|
@@ -127,8 +143,10 @@ export function categoryGuidance(opts) {
|
|
|
127
143
|
}
|
|
128
144
|
lines.push("");
|
|
129
145
|
lines.push("Judge only from what the reviews say. Where the evidence is thin, say so rather than", "producing a confident line the text does not support — a category read that overstates its", "basis is worse than a short one. Prices, ratings and mention counts are in the payload;", "do not re-derive them by hand.");
|
|
130
|
-
|
|
131
|
-
|
|
146
|
+
if (insightsTool) {
|
|
147
|
+
lines.push("");
|
|
148
|
+
lines.push("To show the result in the conversation — products, their reviews, and your read side by", `side — call ${insightsTool} with what you concluded. It costs nothing and`, "makes no further requests.");
|
|
149
|
+
}
|
|
132
150
|
return lines.join("\n");
|
|
133
151
|
}
|
|
134
152
|
/**
|
|
@@ -384,6 +384,48 @@ export const LOADING_PLANS = {
|
|
|
384
384
|
},
|
|
385
385
|
],
|
|
386
386
|
},
|
|
387
|
+
// The same three jobs on the other ten sites. Priced identically because
|
|
388
|
+
// they are the same work — a real browser render behind the site's bot
|
|
389
|
+
// defences — and the note does not name Amazon, because ten of the eleven
|
|
390
|
+
// are not it.
|
|
391
|
+
scan_marketplace_category: {
|
|
392
|
+
label: "Collecting the category",
|
|
393
|
+
kind: "list",
|
|
394
|
+
n: 6,
|
|
395
|
+
steps: [],
|
|
396
|
+
perUnit: [
|
|
397
|
+
{
|
|
398
|
+
via: "scan_marketplace_category",
|
|
399
|
+
label: "Collecting a product",
|
|
400
|
+
detail: "price, stars and its reviews",
|
|
401
|
+
credits: 3,
|
|
402
|
+
arg: "limit",
|
|
403
|
+
defaultCount: 10,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
note: "Each site is collected politely, so this is minutes rather than seconds. Partial results come back either way.",
|
|
407
|
+
},
|
|
408
|
+
// Free: the poll of a collection already paid for.
|
|
409
|
+
marketplace_scan_status: {
|
|
410
|
+
label: "Checking the collection",
|
|
411
|
+
kind: "list",
|
|
412
|
+
n: 6,
|
|
413
|
+
steps: [],
|
|
414
|
+
free: true,
|
|
415
|
+
},
|
|
416
|
+
get_marketplace_product: {
|
|
417
|
+
label: "Reading the product",
|
|
418
|
+
kind: "text",
|
|
419
|
+
n: 1,
|
|
420
|
+
steps: [
|
|
421
|
+
{
|
|
422
|
+
via: "get_marketplace_product",
|
|
423
|
+
label: "Fetching the product",
|
|
424
|
+
detail: "price, stars and its reviews",
|
|
425
|
+
credits: 3,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
},
|
|
387
429
|
// ─── Own-account reads: nooticr's own stored rows ───
|
|
388
430
|
get_scheduled_posts: { label: "Reading your pipeline", kind: "list", n: 5, steps: [], free: true },
|
|
389
431
|
get_post_performance: { label: "Reading your numbers", kind: "list", n: 5, steps: [], free: true },
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OUTPUT_SCHEMAS } from "./output-schemas.js";
|
|
3
|
+
import { viewMeta } from "./view-meta.js";
|
|
4
|
+
import { withEvidence } from "./evidence-digest.js";
|
|
5
|
+
import { confirmSpend, declinedResult } from "./spend.js";
|
|
6
|
+
import { allReviews, categoryGuidance, listingCount, normaliseProducts, scanCost } from "./amazon.js";
|
|
7
|
+
/**
|
|
8
|
+
* The eleven sites, mirroring `MARKETPLACES` in nooticr-server.
|
|
9
|
+
*
|
|
10
|
+
* Kept in the same order and the same words deliberately: a model reads this
|
|
11
|
+
* text to decide what to send, and the server refuses on the same terms. Two
|
|
12
|
+
* registries that drift are how a tool starts describing an argument the
|
|
13
|
+
* collector no longer accepts.
|
|
14
|
+
*/
|
|
15
|
+
export const MARKETS = [
|
|
16
|
+
{
|
|
17
|
+
slug: "amazon",
|
|
18
|
+
label: "Amazon",
|
|
19
|
+
idLabel: "an ASIN or a full Amazon product URL",
|
|
20
|
+
region: {
|
|
21
|
+
field: "domain",
|
|
22
|
+
hint: "a country code or host — com (US), co.uk, de, fr, it, es, ca, co.jp, in, com.br, " +
|
|
23
|
+
"com.mx, com.au, nl, se, pl, com.be, com.tr, ae, sa, eg, sg, cn. Default www.amazon.com",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
slug: "aliexpress",
|
|
28
|
+
label: "AliExpress",
|
|
29
|
+
idLabel: "the numeric id from /item/<id>.html, or a full AliExpress product URL",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
slug: "cdiscount",
|
|
33
|
+
label: "Cdiscount",
|
|
34
|
+
idLabel: "the SKU from a product URL's /f-<category>-<sku>.html, or the full URL",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
slug: "flipkart",
|
|
38
|
+
label: "Flipkart",
|
|
39
|
+
idLabel: "the 16-character PID from a product URL's pid= parameter, or the full URL",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
slug: "mercadolibre",
|
|
43
|
+
label: "Mercado Libre",
|
|
44
|
+
idLabel: "a site id such as MLA904253674, or a full Mercado Libre product URL",
|
|
45
|
+
region: {
|
|
46
|
+
field: "market",
|
|
47
|
+
hint: "one of eighteen markets, named however it comes to hand — MLB, br, Brazil or " +
|
|
48
|
+
"mercadolivre.com.br. Inferred from the ids when omitted, and a job runs against one " +
|
|
49
|
+
"market: an id from another is refused rather than mis-filed",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
slug: "lazada",
|
|
54
|
+
label: "Lazada",
|
|
55
|
+
idLabel: "the number after -i in a product URL (13711387806 in /products/<slug>-i13711387806.html), " +
|
|
56
|
+
"or the full URL",
|
|
57
|
+
region: {
|
|
58
|
+
field: "market",
|
|
59
|
+
hint: "sg, my, th, vn, ph or id. An item id is scoped to one of them — the same id on another " +
|
|
60
|
+
"Lazada host is a different product or nothing at all",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
slug: "otto",
|
|
65
|
+
label: "Otto",
|
|
66
|
+
idLabel: "the tail of the product path — C1720803139 on otto.de, or the SKU AKLBB2029107352 on " +
|
|
67
|
+
"ottoversand.at",
|
|
68
|
+
region: {
|
|
69
|
+
field: "market",
|
|
70
|
+
hint: "de or at. The two are not interchangeable: an id from one does not address a product " +
|
|
71
|
+
"on the other",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
slug: "rakuten",
|
|
76
|
+
label: "Rakuten",
|
|
77
|
+
idLabel: "on Ichiba `<shopId>_<itemId>` (411453_10000003) or `<shop>/<manageNumber>`; on Taiwan " +
|
|
78
|
+
"`<shop>/<productCode>`. A full product URL works too",
|
|
79
|
+
region: {
|
|
80
|
+
field: "market",
|
|
81
|
+
hint: "jp (Ichiba) or tw. Taiwan publishes no review text, so a category study there reads on " +
|
|
82
|
+
"price and rating alone",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
slug: "trendyol",
|
|
87
|
+
label: "Trendyol",
|
|
88
|
+
idLabel: "the number after -p- in a product URL (819066262 in /brand/name-p-819066262), or the full URL",
|
|
89
|
+
region: {
|
|
90
|
+
field: "market",
|
|
91
|
+
hint: "one of 47 storefronts — tr, de, ae, sa, at, be, fr, it, nl, pl, ro and the rest. An id " +
|
|
92
|
+
"is scoped to one: the same number 404s on another country's",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
slug: "jumia",
|
|
97
|
+
label: "Jumia",
|
|
98
|
+
idLabel: "a SKU such as OR537EA86PWGTNAFAMZ. A product URL does NOT contain it — the number at the " +
|
|
99
|
+
"end of a product path is a different handle — so reach a product through `query` instead",
|
|
100
|
+
region: { field: "market", hint: "ng, eg, ke, ma, ci, gh, sn, tn, ug or dz" },
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
slug: "temu",
|
|
104
|
+
label: "Temu",
|
|
105
|
+
idLabel: "the goods id after -g- in a product URL, or the full URL",
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
const BY_SLUG = new Map(MARKETS.map((m) => [m.slug, m]));
|
|
109
|
+
export function marketOf(slug) {
|
|
110
|
+
return typeof slug === "string" ? BY_SLUG.get(slug.toLowerCase().trim()) : undefined;
|
|
111
|
+
}
|
|
112
|
+
/** "amazon | aliexpress | ..." — the set, for a description. */
|
|
113
|
+
const SLUGS = MARKETS.map((m) => m.slug).join(", ");
|
|
114
|
+
/**
|
|
115
|
+
* The storefront argument, described site by site.
|
|
116
|
+
*
|
|
117
|
+
* One string rather than eleven schemas, because a tool takes one `marketplace`
|
|
118
|
+
* per call and the host shows the whole description whichever it picks. Sites
|
|
119
|
+
* with a single storefront are named as taking none, so a model does not
|
|
120
|
+
* invent one for Temu.
|
|
121
|
+
*/
|
|
122
|
+
const MARKET_HINT = MARKETS.filter((m) => m.region)
|
|
123
|
+
.map((m) => `${m.label}: ${m.region.hint}`)
|
|
124
|
+
.join(" · ") +
|
|
125
|
+
` · ${MARKETS.filter((m) => !m.region).map((m) => m.label).join(", ")} have one storefront each ` +
|
|
126
|
+
"and take no market argument.";
|
|
127
|
+
/** The product ids each site addresses by, for `items`. */
|
|
128
|
+
const ID_HINT = MARKETS.map((m) => `${m.label}: ${m.idLabel}`).join(" · ");
|
|
129
|
+
function structured(res) {
|
|
130
|
+
return (res.structured ?? {});
|
|
131
|
+
}
|
|
132
|
+
function toolError(what, err) {
|
|
133
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
134
|
+
return { isError: true, content: [{ type: "text", text: `${what}: ${message}` }] };
|
|
135
|
+
}
|
|
136
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
|
|
137
|
+
/**
|
|
138
|
+
* The scan, in the shape the view and the digest read.
|
|
139
|
+
*
|
|
140
|
+
* The Amazon twin of this hardcodes its own slug and `domain`; a scan here
|
|
141
|
+
* carries whichever site it ran against and the storefront under the one name
|
|
142
|
+
* `market`, because each collector calls it something different and a caller
|
|
143
|
+
* should not have to know which.
|
|
144
|
+
*/
|
|
145
|
+
function marketplaceResult(payload, m, focus) {
|
|
146
|
+
const products = normaliseProducts(payload.products);
|
|
147
|
+
const rollup = (payload.rollup ?? {});
|
|
148
|
+
const reviews = allReviews(products);
|
|
149
|
+
const progress = (payload.progress ?? {});
|
|
150
|
+
const total = num(progress.total) ?? products.length;
|
|
151
|
+
const done = num(progress.done) ?? products.length;
|
|
152
|
+
const label = String(payload.query ?? "") ||
|
|
153
|
+
(products.length === 1 ? String(products[0]?.title ?? "this listing") : "this set of listings");
|
|
154
|
+
const guidance = categoryGuidance({
|
|
155
|
+
label,
|
|
156
|
+
products: products.length,
|
|
157
|
+
reviews: reviews.length,
|
|
158
|
+
ratingsRepresented: num(rollup.ratingsRepresented) ?? 0,
|
|
159
|
+
brands: Array.isArray(rollup.brands) ? rollup.brands : [],
|
|
160
|
+
complete: payload.complete !== false,
|
|
161
|
+
scanId: String(payload.scanId ?? ""),
|
|
162
|
+
pending: Math.max(0, total - done),
|
|
163
|
+
focus,
|
|
164
|
+
site: m.label,
|
|
165
|
+
statusTool: "marketplace_scan_status",
|
|
166
|
+
// There is no free insights view for these sites yet, and pointing a model
|
|
167
|
+
// at show_amazon_category_insights would have it draw a Lazada scan as an
|
|
168
|
+
// Amazon one. Better to say nothing than to name the wrong tool.
|
|
169
|
+
insightsTool: null,
|
|
170
|
+
});
|
|
171
|
+
const structuredPayload = {
|
|
172
|
+
guidance,
|
|
173
|
+
mode: "evidence",
|
|
174
|
+
marketplace: m.slug,
|
|
175
|
+
scanId: payload.scanId ?? null,
|
|
176
|
+
status: payload.status ?? null,
|
|
177
|
+
complete: payload.complete !== false,
|
|
178
|
+
query: payload.query ?? null,
|
|
179
|
+
market: payload.market ?? payload.domain ?? null,
|
|
180
|
+
progress,
|
|
181
|
+
rollup,
|
|
182
|
+
products,
|
|
183
|
+
reviews,
|
|
184
|
+
errors: payload.errors ?? [],
|
|
185
|
+
mcpCredits: payload.mcpCredits ?? null,
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: withEvidence(guidance, structuredPayload) }],
|
|
189
|
+
structuredContent: structuredPayload,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/** The `marketplace` argument, shared by all three tools. */
|
|
193
|
+
const marketplaceArg = z
|
|
194
|
+
.enum(MARKETS.map((m) => m.slug))
|
|
195
|
+
.describe(`Which site to read. One of: ${SLUGS}.`);
|
|
196
|
+
export function registerMarketplaceTools(server, makeClient) {
|
|
197
|
+
server.registerTool("scan_marketplace_category", {
|
|
198
|
+
title: "Scan Marketplace Category",
|
|
199
|
+
_meta: viewMeta("scan_marketplace_category"),
|
|
200
|
+
description: "Collect a category from any of eleven marketplaces and read it: products with prices, " +
|
|
201
|
+
"star histograms, the review text itself, and the arithmetic over them (price spread, " +
|
|
202
|
+
"rating spread, which aspects recur across how many brands, and the share of each " +
|
|
203
|
+
"product's ratings sitting at 1-2 stars). `marketplace` picks the site — one of " +
|
|
204
|
+
`${SLUGS}. ` +
|
|
205
|
+
"Pass `query` to search the category, `items` to pin named competitors by id or URL, or " +
|
|
206
|
+
"both. You write the read: purchase drivers, barriers, what the incumbents do well, the " +
|
|
207
|
+
"gaps, and how a new entrant could position. " +
|
|
208
|
+
"Costs 3 nooticr credits per product collected — a product with reviews is a real browser " +
|
|
209
|
+
"render behind the site's bot defences. " +
|
|
210
|
+
"Collection runs in the background: this returns everything ready within `waitSeconds` " +
|
|
211
|
+
"plus a scanId to continue with marketplace_scan_status, which is free and worth " +
|
|
212
|
+
"repeating until the scan is done. " +
|
|
213
|
+
"For Amazon specifically, scan_amazon_category returns the same shape plus Amazon's own " +
|
|
214
|
+
"review-aspect counts.",
|
|
215
|
+
annotations: {
|
|
216
|
+
readOnlyHint: true,
|
|
217
|
+
destructiveHint: false,
|
|
218
|
+
idempotentHint: false,
|
|
219
|
+
openWorldHint: true,
|
|
220
|
+
},
|
|
221
|
+
outputSchema: OUTPUT_SCHEMAS.scan_marketplace_category,
|
|
222
|
+
inputSchema: z
|
|
223
|
+
.object({
|
|
224
|
+
marketplace: marketplaceArg,
|
|
225
|
+
query: z.string().optional().describe("Category or keyword to search, e.g. 'cast iron skillet'."),
|
|
226
|
+
items: z
|
|
227
|
+
.array(z.string())
|
|
228
|
+
.optional()
|
|
229
|
+
.describe(`Named competitors, by product id or URL. ${ID_HINT}`),
|
|
230
|
+
limit: z.number().int().optional().describe("Products to collect (default 10, max 50)."),
|
|
231
|
+
reviews: z
|
|
232
|
+
.boolean()
|
|
233
|
+
.optional()
|
|
234
|
+
.describe("Collect review text too (default true). False is faster and much cheaper to run, " +
|
|
235
|
+
"but leaves only the aggregates."),
|
|
236
|
+
market: z
|
|
237
|
+
.string()
|
|
238
|
+
.optional()
|
|
239
|
+
.describe(`The storefront to read, for the sites that have more than one. ${MARKET_HINT}`),
|
|
240
|
+
domain: z
|
|
241
|
+
.string()
|
|
242
|
+
.optional()
|
|
243
|
+
.describe("Amazon only: the marketplace host, e.g. www.amazon.co.uk. Other sites use `market`."),
|
|
244
|
+
waitSeconds: z
|
|
245
|
+
.number()
|
|
246
|
+
.int()
|
|
247
|
+
.optional()
|
|
248
|
+
.describe("How long to wait before returning partial results (default 45, max 55). " +
|
|
249
|
+
"Capped under the 60s at which hosts abandon a tool call — asking for longer " +
|
|
250
|
+
"returns nothing and loses the scanId, not a bigger answer. " +
|
|
251
|
+
"Poll marketplace_scan_status for the rest instead; it is free and repeatable."),
|
|
252
|
+
focus: z
|
|
253
|
+
.string()
|
|
254
|
+
.optional()
|
|
255
|
+
.describe("An extra question to answer from the reviews, e.g. 'what do buyers say about sizing?'"),
|
|
256
|
+
})
|
|
257
|
+
.strict(),
|
|
258
|
+
}, async (args, extra) => {
|
|
259
|
+
const client = await makeClient({ ...extra, arguments: args });
|
|
260
|
+
const m = marketOf(args.marketplace);
|
|
261
|
+
if (!m)
|
|
262
|
+
return toolError("scan_marketplace_category failed", new Error(`unknown marketplace — one of ${SLUGS}`));
|
|
263
|
+
try {
|
|
264
|
+
// `focus` steers the guidance, not the collection — sending it upstream
|
|
265
|
+
// would be an argument the backend silently drops.
|
|
266
|
+
const { focus, ...upstream } = args;
|
|
267
|
+
// Priced and confirmed before the scan starts, on the same terms as
|
|
268
|
+
// the Amazon one: a started scan has already spent the expensive half.
|
|
269
|
+
const credits = scanCost(args);
|
|
270
|
+
const decision = await confirmSpend(server.server, {
|
|
271
|
+
credits,
|
|
272
|
+
summary: `Collect ${listingCount(args)} ${m.label} product(s)` +
|
|
273
|
+
(typeof args.query === "string" && args.query ? ` for "${args.query}"` : "") +
|
|
274
|
+
`, with their reviews.`,
|
|
275
|
+
cheaper: 'Lower "limit", or name the competitors you care about in "items".',
|
|
276
|
+
});
|
|
277
|
+
if (!decision.proceed) {
|
|
278
|
+
return declinedResult(credits, `Collecting ${listingCount(args)} ${m.label} product(s)`, 'Lower "limit", or name the competitors you care about in "items".');
|
|
279
|
+
}
|
|
280
|
+
const res = await client.callTool("scan_marketplace_category", upstream);
|
|
281
|
+
return marketplaceResult(structured(res), m, typeof focus === "string" ? focus : undefined);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
return toolError("scan_marketplace_category failed", err);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
server.registerTool("marketplace_scan_status", {
|
|
288
|
+
title: "Marketplace Scan Status",
|
|
289
|
+
_meta: viewMeta("marketplace_scan_status"),
|
|
290
|
+
description: "Pick up a scan_marketplace_category collection that was still running, by scanId and the " +
|
|
291
|
+
"same marketplace — the products and reviews collected since, with the same rollup and " +
|
|
292
|
+
"the same instructions for reading them. Free: it is the poll scan_marketplace_category " +
|
|
293
|
+
"asked for, and charging for the second half of one answer would bill a wait this server " +
|
|
294
|
+
"chose. Call it when a scan came back incomplete, then write your read from the full set.",
|
|
295
|
+
annotations: {
|
|
296
|
+
readOnlyHint: true,
|
|
297
|
+
destructiveHint: false,
|
|
298
|
+
idempotentHint: true,
|
|
299
|
+
openWorldHint: true,
|
|
300
|
+
},
|
|
301
|
+
outputSchema: OUTPUT_SCHEMAS.marketplace_scan_status,
|
|
302
|
+
inputSchema: z
|
|
303
|
+
.object({
|
|
304
|
+
marketplace: marketplaceArg,
|
|
305
|
+
scanId: z.string().describe("The scanId scan_marketplace_category returned."),
|
|
306
|
+
waitSeconds: z
|
|
307
|
+
.number()
|
|
308
|
+
.int()
|
|
309
|
+
.optional()
|
|
310
|
+
.describe("How long to wait for more products before answering (default 30, max 55). " +
|
|
311
|
+
"Call it again as often as you need rather than asking for one long wait."),
|
|
312
|
+
focus: z.string().optional().describe("Same optional steer as scan_marketplace_category."),
|
|
313
|
+
})
|
|
314
|
+
.strict(),
|
|
315
|
+
}, async (args, extra) => {
|
|
316
|
+
const client = await makeClient({ ...extra, arguments: args });
|
|
317
|
+
const m = marketOf(args.marketplace);
|
|
318
|
+
if (!m)
|
|
319
|
+
return toolError("marketplace_scan_status failed", new Error(`unknown marketplace — one of ${SLUGS}`));
|
|
320
|
+
try {
|
|
321
|
+
const { focus, ...upstream } = args;
|
|
322
|
+
const res = await client.callTool("marketplace_scan_status", upstream);
|
|
323
|
+
return marketplaceResult(structured(res), m, typeof focus === "string" ? focus : undefined);
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
return toolError("marketplace_scan_status failed", err);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
server.registerTool("get_marketplace_product", {
|
|
330
|
+
title: "Get Marketplace Product",
|
|
331
|
+
_meta: viewMeta("get_marketplace_product"),
|
|
332
|
+
description: "Fetch one product from any of eleven marketplaces by id or URL: price, rating, the full " +
|
|
333
|
+
"star histogram, specs, and the review text. `marketplace` picks the site — one of " +
|
|
334
|
+
`${SLUGS}. ` +
|
|
335
|
+
"Costs 3 nooticr credits. " +
|
|
336
|
+
"Use for a single product; for a category or a competitive set, scan_marketplace_category " +
|
|
337
|
+
"collects many in one job and rolls them up.",
|
|
338
|
+
annotations: {
|
|
339
|
+
readOnlyHint: true,
|
|
340
|
+
destructiveHint: false,
|
|
341
|
+
idempotentHint: true,
|
|
342
|
+
openWorldHint: true,
|
|
343
|
+
},
|
|
344
|
+
outputSchema: OUTPUT_SCHEMAS.get_marketplace_product,
|
|
345
|
+
inputSchema: z
|
|
346
|
+
.object({
|
|
347
|
+
marketplace: marketplaceArg,
|
|
348
|
+
id: z.string().optional().describe(`The product id. ${ID_HINT}`),
|
|
349
|
+
url: z.string().optional().describe("The full product URL, if you have that instead."),
|
|
350
|
+
reviews: z.boolean().optional().describe("Collect review text too (default true)."),
|
|
351
|
+
market: z
|
|
352
|
+
.string()
|
|
353
|
+
.optional()
|
|
354
|
+
.describe(`The storefront the product is on, for sites that have more than one. ${MARKET_HINT}`),
|
|
355
|
+
domain: z
|
|
356
|
+
.string()
|
|
357
|
+
.optional()
|
|
358
|
+
.describe("Amazon only: the marketplace host. Other sites use `market`."),
|
|
359
|
+
})
|
|
360
|
+
.strict(),
|
|
361
|
+
}, async (args, extra) => {
|
|
362
|
+
const client = await makeClient({ ...extra, arguments: args });
|
|
363
|
+
const m = marketOf(args.marketplace);
|
|
364
|
+
if (!m)
|
|
365
|
+
return toolError("get_marketplace_product failed", new Error(`unknown marketplace — one of ${SLUGS}`));
|
|
366
|
+
try {
|
|
367
|
+
const res = await client.callTool("get_marketplace_product", args);
|
|
368
|
+
return marketplaceResult(structured(res), m);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
return toolError("get_marketplace_product failed", err);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
}
|
|
@@ -293,6 +293,16 @@ const amazonScanShape = {
|
|
|
293
293
|
errors: anyList(),
|
|
294
294
|
mcpCredits,
|
|
295
295
|
};
|
|
296
|
+
/**
|
|
297
|
+
* A marketplace scan: the Amazon shape with the storefront reported under one
|
|
298
|
+
* name. `domain` is Amazon's word for it and the others each have their own,
|
|
299
|
+
* so the generic tools answer with `market` whichever site ran.
|
|
300
|
+
*/
|
|
301
|
+
const marketplaceScanShape = {
|
|
302
|
+
...amazonScanShape,
|
|
303
|
+
market: scalar().describe("The storefront this ran against, however the site names it."),
|
|
304
|
+
};
|
|
305
|
+
const marketplaceScan = open(marketplaceScanShape);
|
|
296
306
|
const amazonScan = open(amazonScanShape);
|
|
297
307
|
export const OUTPUT_SCHEMAS = {
|
|
298
308
|
analyze_post: open({ ...analyzed, ...evidence }),
|
|
@@ -1378,6 +1388,17 @@ export const OUTPUT_SCHEMAS = {
|
|
|
1378
1388
|
// names for one payload and three places for the view to drift.
|
|
1379
1389
|
scan_amazon_category: amazonScan,
|
|
1380
1390
|
amazon_scan_status: amazonScan,
|
|
1391
|
+
// The same shape on ten more sites. `marketplace` says which one and
|
|
1392
|
+
// `market` which of its storefronts — the Amazon pair reports the storefront
|
|
1393
|
+
// as `domain`, because that is what Amazon calls it, and every other
|
|
1394
|
+
// collector names it differently again. One key here, so a caller reading
|
|
1395
|
+
// the result does not have to know which site it asked.
|
|
1396
|
+
scan_marketplace_category: marketplaceScan,
|
|
1397
|
+
marketplace_scan_status: marketplaceScan,
|
|
1398
|
+
get_marketplace_product: open({
|
|
1399
|
+
...marketplaceScanShape,
|
|
1400
|
+
product: amazonProduct.nullish().describe("The single product, also present as products[0]."),
|
|
1401
|
+
}),
|
|
1381
1402
|
get_amazon_product: open({
|
|
1382
1403
|
...amazonScanShape,
|
|
1383
1404
|
product: amazonProduct.nullish().describe("The single listing, also present as products[0]."),
|
package/dist/shared/tools-def.js
CHANGED
|
@@ -432,6 +432,24 @@ export const TOOL_DEFINITIONS = [
|
|
|
432
432
|
description: "Fetch one Amazon listing by ASIN or URL: price, rating, the full star histogram, features, specs, Amazon's review digest and aspect breakdown, and the review text. Use for a single product; for a category or a competitive set, scan_amazon_category collects many in one job and rolls them up. Consumes 3 nooticr credits.",
|
|
433
433
|
inputSchema: z.object({ asin: z.string().optional().describe("ASIN, or a full Amazon product URL."), url: z.string().optional().describe("Full Amazon product URL, if you have that instead."), reviews: z.boolean().optional().describe("Collect review text too (default true)."), domain: z.string().optional().describe("Marketplace host (default www.amazon.com).") }).strict(),
|
|
434
434
|
},
|
|
435
|
+
{
|
|
436
|
+
name: "scan_marketplace_category",
|
|
437
|
+
title: "Scan Marketplace Category",
|
|
438
|
+
description: "Collect a category from any of eleven marketplaces and read it: products with prices, star histograms, the review text itself, and the arithmetic over them (price spread, rating spread, which aspects recur across how many brands, and the share of each product's ratings sitting at 1-2 stars). marketplace picks the site — one of amazon, aliexpress, cdiscount, flipkart, mercadolibre, lazada, otto, rakuten, trendyol, jumia, temu. Pass query to search the category, items to pin named competitors by id or URL, or both. You write the read: purchase drivers, barriers, what the incumbents do well, the gaps, and how a new entrant could position. Collection runs in the background, so this returns everything ready within waitSeconds plus a scanId to continue with marketplace_scan_status — that is free, and repeating it is how a scan gets more time. For Amazon specifically, scan_amazon_category returns the same shape plus Amazon's own review-aspect counts. Consumes 3 nooticr credits per product collected — a product with reviews is a real browser render behind the site's bot defences.",
|
|
439
|
+
inputSchema: z.object({ marketplace: z.string().describe("Which site to read. One of: amazon, aliexpress, cdiscount, flipkart, mercadolibre, lazada, otto, rakuten, trendyol, jumia, temu."), query: z.string().optional().describe("Category or keyword to search, e.g. 'cast iron skillet'."), items: z.array(z.string()).optional().describe("Named competitors, by product id or URL."), limit: z.number().int().optional().describe("Products to collect (default 10, max 50)."), reviews: z.boolean().optional().describe("Collect review text too (default true)."), market: z.string().optional().describe("The storefront to read, for the sites that have more than one. Amazon: a country code or host — com (US), co.uk, de, fr and the rest, default www.amazon.com · Mercado Libre: one of eighteen markets, named however it comes to hand — MLB, br, Brazil · Lazada: sg, my, th, vn, ph or id · Otto: de or at · Rakuten: jp (Ichiba) or tw · Trendyol: one of 47 storefronts — tr, de, ae, sa and the rest · Jumia: ng, eg, ke, ma, ci, gh, sn, tn, ug or dz · AliExpress, Cdiscount, Flipkart and Temu have one storefront each and take no market argument. An id is scoped to one storefront: the same id on another is a different product or nothing at all."), domain: z.string().optional().describe("Amazon only: the marketplace host. Other sites use market."), waitSeconds: z.number().int().optional().describe("How long to wait before returning partial results (default 45, max 55). Poll marketplace_scan_status for the rest instead; it is free and repeatable."), focus: z.string().optional().describe("An extra question to answer from the reviews.") }).strict(),
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "marketplace_scan_status",
|
|
443
|
+
title: "Marketplace Scan Status",
|
|
444
|
+
description: "Pick up a scan_marketplace_category collection that was still running, by scanId and the same marketplace — the products and reviews collected since, with the same rollup and the same instructions for reading them. Free: it is the poll scan_marketplace_category asked for, and charging for the second half of one answer would bill a wait this server chose. Call it when a scan came back incomplete, then write your read from the full set.",
|
|
445
|
+
inputSchema: z.object({ marketplace: z.string().describe("The same site the scan ran against."), scanId: z.string().describe("The scanId scan_marketplace_category returned."), waitSeconds: z.number().int().optional().describe("How long to wait for more products before answering (default 30, max 55)."), focus: z.string().optional().describe("Same optional steer as scan_marketplace_category.") }).strict(),
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "get_marketplace_product",
|
|
449
|
+
title: "Get Marketplace Product",
|
|
450
|
+
description: "Fetch one product from any of eleven marketplaces by id or URL: price, rating, the full star histogram, specs, and the review text. marketplace picks the site — one of amazon, aliexpress, cdiscount, flipkart, mercadolibre, lazada, otto, rakuten, trendyol, jumia, temu. Use for a single product; for a category or a competitive set, scan_marketplace_category collects many in one job and rolls them up. Consumes 3 nooticr credits.",
|
|
451
|
+
inputSchema: z.object({ marketplace: z.string().describe("Which site the product is on. One of: amazon, aliexpress, cdiscount, flipkart, mercadolibre, lazada, otto, rakuten, trendyol, jumia, temu."), id: z.string().optional().describe("The product id, in whatever form the site addresses products by."), url: z.string().optional().describe("The full product URL, if you have that instead."), reviews: z.boolean().optional().describe("Collect review text too (default true)."), market: z.string().optional().describe("The storefront the product is on, for sites that have more than one. Amazon: a country code or host — com (US), co.uk, de, fr and the rest, default www.amazon.com · Mercado Libre: one of eighteen markets, named however it comes to hand — MLB, br, Brazil · Lazada: sg, my, th, vn, ph or id · Otto: de or at · Rakuten: jp (Ichiba) or tw · Trendyol: one of 47 storefronts — tr, de, ae, sa and the rest · Jumia: ng, eg, ke, ma, ci, gh, sn, tn, ug or dz · AliExpress, Cdiscount, Flipkart and Temu have one storefront each and take no market argument. An id is scoped to one storefront: the same id on another is a different product or nothing at all."), domain: z.string().optional().describe("Amazon only: the marketplace host. Other sites use market.") }).strict(),
|
|
452
|
+
},
|
|
435
453
|
{
|
|
436
454
|
name: "show_amazon_category_insights",
|
|
437
455
|
title: "Show Amazon Category Insights",
|
package/dist/shared/tools.js
CHANGED
|
@@ -26,8 +26,9 @@ import { loadingPlansJson } from "./loading-plans.js";
|
|
|
26
26
|
import { registerHandoff } from "./handoff.js";
|
|
27
27
|
import { registerCollabTools } from "./collab.js";
|
|
28
28
|
import { registerAmazonTools } from "./amazon.js";
|
|
29
|
+
import { registerMarketplaceTools } from "./marketplace.js";
|
|
29
30
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
30
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
31
|
+
export const MCP_SERVER_VERSION = "1.26.38";
|
|
31
32
|
/** MCP Apps extension identifier */
|
|
32
33
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
33
34
|
/** MIME type for MCP Apps HTML resources */
|
|
@@ -645,6 +646,12 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
645
646
|
"amazon_scan_status",
|
|
646
647
|
"get_amazon_product",
|
|
647
648
|
"show_amazon_category_insights",
|
|
649
|
+
// The marketplace-agnostic trio. Every name here gets a ui://nooticr/<tool>
|
|
650
|
+
// resource, which is what gives a tool its card — a tool missing from this
|
|
651
|
+
// list is one the host is told to render and then 404s on.
|
|
652
|
+
"scan_marketplace_category",
|
|
653
|
+
"marketplace_scan_status",
|
|
654
|
+
"get_marketplace_product",
|
|
648
655
|
];
|
|
649
656
|
// Human-readable resource name per tool (used in resources/list + tools/list).
|
|
650
657
|
function resourceName(tool) {
|
|
@@ -2387,5 +2394,10 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
2387
2394
|
// that picks it back up, a single listing, and the free view for the read
|
|
2388
2395
|
// the model writes out of them.
|
|
2389
2396
|
registerAmazonTools(server, makeClient);
|
|
2397
|
+
// The same three jobs on the other ten sites (marketplace.ts). Registered
|
|
2398
|
+
// separately because Amazon's collector returns its own review-aspect counts
|
|
2399
|
+
// and the generic one cannot, so the two descriptions are not the same
|
|
2400
|
+
// promise — not because the question differs.
|
|
2401
|
+
registerMarketplaceTools(server, makeClient);
|
|
2390
2402
|
return server;
|
|
2391
2403
|
}
|
|
@@ -51,7 +51,13 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
51
51
|
list_social_connections:"Connected Accounts",
|
|
52
52
|
connect_social_account:"Connect an Account",
|
|
53
53
|
create_product:"Add a Product",
|
|
54
|
-
update_product:"Update a Product"
|
|
54
|
+
update_product:"Update a Product",
|
|
55
|
+
scan_amazon_category:"Scan Amazon Category",
|
|
56
|
+
amazon_scan_status:"Amazon Scan Status",
|
|
57
|
+
get_amazon_product:"Get Amazon Product",
|
|
58
|
+
scan_marketplace_category:"Scan Marketplace Category",
|
|
59
|
+
marketplace_scan_status:"Marketplace Scan Status",
|
|
60
|
+
get_marketplace_product:"Get Marketplace Product"
|
|
55
61
|
};
|
|
56
62
|
var TOOL_DESCS={
|
|
57
63
|
analyze_post:"Frames from the post plus its transcript, for you to read yourself.",
|
|
@@ -74,7 +80,13 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
74
80
|
list_social_connections:"What you have connected, and what each connection is allowed to do.",
|
|
75
81
|
connect_social_account:"A link you open to connect one account. Nothing connects until you approve it.",
|
|
76
82
|
create_product:"Create a product in your own workspace — the row every other own-account tool needs.",
|
|
77
|
-
update_product:"Patch a product's fields. An omitted field keeps the value it had."
|
|
83
|
+
update_product:"Patch a product's fields. An omitted field keeps the value it had.",
|
|
84
|
+
scan_amazon_category:"A category on Amazon — prices, ratings and what reviewers keep raising.",
|
|
85
|
+
amazon_scan_status:"The listings an Amazon scan has collected so far.",
|
|
86
|
+
get_amazon_product:"One Amazon listing: price, rating, star histogram and reviews.",
|
|
87
|
+
scan_marketplace_category:"A category on any of eleven marketplaces — prices, ratings and review themes.",
|
|
88
|
+
marketplace_scan_status:"The products a marketplace scan has collected so far.",
|
|
89
|
+
get_marketplace_product:"One product from any marketplace: price, rating, histogram and reviews."
|
|
78
90
|
};
|
|
79
91
|
|
|
80
92
|
// ─── The mark ───
|
|
@@ -3604,16 +3616,59 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3604
3616
|
+'<path fill="#FF9900" d="'+d+'"></path></svg>';
|
|
3605
3617
|
}
|
|
3606
3618
|
|
|
3619
|
+
/**
|
|
3620
|
+
* Each site's own colours, for the mark beside the header.
|
|
3621
|
+
*
|
|
3622
|
+
* Initials on the brand's colour rather than its logo. Amazon's mark is here
|
|
3623
|
+
* because Font Awesome publishes the path under CC BY 4.0; the other ten do
|
|
3624
|
+
* not have one to hand, and drawing a brand's wordmark from memory produces
|
|
3625
|
+
* a wrong logo rather than no logo. An <img> is not an option either: this
|
|
3626
|
+
* view is one self-contained file in a sandboxed iframe with no remote
|
|
3627
|
+
* fetches, so every asset is inline or it does not render.
|
|
3628
|
+
*/
|
|
3629
|
+
var MARKET_BRAND={
|
|
3630
|
+
amazon:{label:"Amazon",color:"#FF9900",on:"#111"},
|
|
3631
|
+
aliexpress:{label:"AliExpress",color:"#E62E04",on:"#fff"},
|
|
3632
|
+
cdiscount:{label:"Cdiscount",color:"#C4161C",on:"#fff"},
|
|
3633
|
+
flipkart:{label:"Flipkart",color:"#2874F0",on:"#fff"},
|
|
3634
|
+
mercadolibre:{label:"Mercado Libre",color:"#FFE600",on:"#111"},
|
|
3635
|
+
lazada:{label:"Lazada",color:"#0F146D",on:"#fff"},
|
|
3636
|
+
otto:{label:"Otto",color:"#E4001B",on:"#fff"},
|
|
3637
|
+
rakuten:{label:"Rakuten",color:"#BF0000",on:"#fff"},
|
|
3638
|
+
trendyol:{label:"Trendyol",color:"#F27A1A",on:"#fff"},
|
|
3639
|
+
jumia:{label:"Jumia",color:"#F68B1E",on:"#111"},
|
|
3640
|
+
temu:{label:"Temu",color:"#FB7701",on:"#fff"}
|
|
3641
|
+
};
|
|
3642
|
+
|
|
3643
|
+
/** What to call the site a payload came from. */
|
|
3644
|
+
function marketLabel(name){
|
|
3645
|
+
var b=MARKET_BRAND[String(name||"").toLowerCase()];
|
|
3646
|
+
return b?b.label:(name?String(name):"Marketplace");
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3607
3649
|
/** The mark for whichever marketplace a payload came from. */
|
|
3608
3650
|
function marketplaceMark(name,size){
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3651
|
+
var key=String(name||"").toLowerCase();
|
|
3652
|
+
if(key==="amazon")return amazonMark(size);
|
|
3653
|
+
var b=MARKET_BRAND[key];
|
|
3654
|
+
var initials=(b?b.label:String(name||"market")).replace(/[^A-Za-z]/g,"").slice(0,2).toUpperCase();
|
|
3655
|
+
return '<span class="mkt-generic" style="display:inline-flex;align-items:center;justify-content:center;'
|
|
3656
|
+
+"width:"+size+"px;height:"+size+"px;border-radius:5px;font-weight:800;line-height:1;"
|
|
3657
|
+
+"background:"+(b?b.color:"#6B7280")+";color:"+(b?b.on:"#fff")+";"
|
|
3658
|
+
+'font-size:'+Math.round(size*0.44)+'px" aria-label="'+esc(marketLabel(name))+'" role="img">'
|
|
3659
|
+
+esc(initials)+"</span>";
|
|
3612
3660
|
}
|
|
3613
3661
|
|
|
3614
|
-
/**
|
|
3662
|
+
/**
|
|
3663
|
+
* "www.amazon.co.uk" reads as a hostname; "amazon.co.uk" reads as a market.
|
|
3664
|
+
*
|
|
3665
|
+
* Empty rather than "amazon.com" when a site did not report one: printing
|
|
3666
|
+
* Amazon's default under a Lazada scan states a storefront the scan never
|
|
3667
|
+
* ran against.
|
|
3668
|
+
*/
|
|
3615
3669
|
function marketDomain(domain){
|
|
3616
|
-
var d=String(domain||"
|
|
3670
|
+
var d=String(domain||"");
|
|
3671
|
+
if(!d)return "";
|
|
3617
3672
|
return d.indexOf("www.")===0?d.slice(4):d;
|
|
3618
3673
|
}
|
|
3619
3674
|
|
|
@@ -3701,7 +3756,9 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3701
3756
|
var open=amazonState.open===String(p.asin||"");
|
|
3702
3757
|
var neg=null;
|
|
3703
3758
|
(((amazonState.rollup||{}).perProduct)||[]).forEach(function(r){
|
|
3704
|
-
|
|
3759
|
+
// The Amazon rollup keys each row by "asin" and the generic one by "id";
|
|
3760
|
+
// reading only the first left every other site without its 1-2 star share.
|
|
3761
|
+
if(String(r.asin||r.id||"")===String(p.asin||""))neg=r.negativeStarShare;
|
|
3705
3762
|
});
|
|
3706
3763
|
return '<button type="button" class="amz-tile'+(open?" amz-tile-open":"")+'" data-amz-open="'+esc(p.asin||"")+'">'
|
|
3707
3764
|
+productThumb(p)
|
|
@@ -4021,7 +4078,10 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4021
4078
|
:((insights&&amazonState)?(amazonState.rollup||{}):{});
|
|
4022
4079
|
amazonState={
|
|
4023
4080
|
marketplace:String(d.marketplace||"amazon"),
|
|
4024
|
-
domain
|
|
4081
|
+
// "domain" is Amazon's word for the storefront and "market" is what the
|
|
4082
|
+
// generic collectors answer with. Defaulting to www.amazon.com meant a
|
|
4083
|
+
// Lazada scan's header read "amazon.com".
|
|
4084
|
+
domain:String(d.domain||d.market||(amazonState&&amazonState.domain)||""),
|
|
4025
4085
|
title:insights?String(d.category||"Category read"):String(d.query||"Category scan"),
|
|
4026
4086
|
products:carried,
|
|
4027
4087
|
rollup:rollup,
|
|
@@ -4048,7 +4108,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4048
4108
|
// distinctive — it names its marketplace — and because an insights payload
|
|
4049
4109
|
// carries a products array of listings, not posts, which the gallery below
|
|
4050
4110
|
// would happily draw as blank cards.
|
|
4051
|
-
if(
|
|
4111
|
+
if(d.marketplace){renderMarketplace(d);return;}
|
|
4052
4112
|
|
|
4053
4113
|
// Own account. Checked before the feeds below because all three shapes are
|
|
4054
4114
|
// distinctive and none of them carries posts, threads or an analysis.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nooticr/mcp",
|
|
3
3
|
"mcpName": "com.nooticr/mcp",
|
|
4
|
-
"version": "1.26.
|
|
4
|
+
"version": "1.26.38",
|
|
5
5
|
"description": "MCP server for nooticr - fetch, discover and understand TikTok/Instagram/YouTube/X/Reddit/LinkedIn/Weibo posts with AI (media metadata, niche discovery, brand monitoring, hook & viral analysis)",
|
|
6
6
|
"homepage": "https://mcp.nooticr.com",
|
|
7
7
|
"repository": {
|