@nooticr/mcp 1.26.39 → 1.26.40
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 +2 -2
- package/dist/shared/amazon.js +39 -1
- package/dist/shared/marketplace.js +54 -5
- package/dist/shared/tools.js +1 -1
- package/dist/shared/ui-template.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,9 +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
|
|
148
|
+
| `scan_marketplace_category` | 3 per product collected (30 by default) | The same category read on eleven more sites — AliExpress, Cdiscount, Flipkart, Mercado Libre, Lazada, Otto, Rakuten, Trendyol, Jumia, Vinted and Temu. Vinted is resale rather than retail, so its `rating` describes the seller and the scan says so in `ratingScope` — do not read it as a product's reviews. `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
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
|
|
150
|
+
| `get_marketplace_product` | 3 | One product from any of the twelve sites, by id or URL: price, rating, the full star histogram, specs and the reviews. |
|
|
151
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. |
|
|
152
152
|
|
|
153
153
|
### Make something
|
package/dist/shared/amazon.js
CHANGED
|
@@ -33,7 +33,25 @@ export function normaliseProducts(raw) {
|
|
|
33
33
|
// raw `p.asin` there gave every review on every non-Amazon product the id
|
|
34
34
|
// "review::0", "review::1" — colliding across products, so a model citing
|
|
35
35
|
// one could not be pointed back at the right listing.
|
|
36
|
-
|
|
36
|
+
//
|
|
37
|
+
// This list must stay equal to `ID_KEYS` in nooticr-server's
|
|
38
|
+
// `crates/server/src/marketplace.rs`. The server computes `rollup.perProduct[].id`
|
|
39
|
+
// from its copy and the view joins those rows onto the tiles by the id
|
|
40
|
+
// resolved here, so a key in one list and not the other does not read as a
|
|
41
|
+
// missing id — it reads as a tile with no `negativeStarShare`, which looks
|
|
42
|
+
// like a site that publishes no histogram.
|
|
43
|
+
//
|
|
44
|
+
// `pid` and `goods_id` were the two missing, which is Flipkart and Temu:
|
|
45
|
+
// each collector publishes exactly one id field and those are theirs, so
|
|
46
|
+
// every product in a scan of either site resolved to "" and shared one
|
|
47
|
+
// blank key — the failure the comment below says was fixed for the others.
|
|
48
|
+
//
|
|
49
|
+
// Found by hand rather than by a test, and `??` is why it could be: it
|
|
50
|
+
// falls through null and undefined but not "", so a site that sends an
|
|
51
|
+
// empty id under an earlier key would stop the chain on the empty value
|
|
52
|
+
// rather than continue past it. Matching on a non-empty string closes
|
|
53
|
+
// both.
|
|
54
|
+
const id = String([p.asin, p.sku, p.pid, p.goods_id, p.item_id, p.product_id, p.id].find((v) => typeof v === "string" && v.trim() !== "") ?? "");
|
|
37
55
|
const histogram = (p.rating_histogram ?? {});
|
|
38
56
|
const reviews = Array.isArray(p.reviews) ? p.reviews : [];
|
|
39
57
|
const aspects = Array.isArray(p.review_aspects) ? p.review_aspects : [];
|
|
@@ -137,6 +155,26 @@ export function categoryGuidance(opts) {
|
|
|
137
155
|
: "."));
|
|
138
156
|
if (opts.brands.length)
|
|
139
157
|
lines.push(`Brands in the set: ${opts.brands.join(", ")}.`);
|
|
158
|
+
// Nothing came back, which is the one outcome where the next step is not
|
|
159
|
+
// "read the set" and the guidance above says nothing useful. Without this a
|
|
160
|
+
// model reports an empty category as a finding — the category is empty —
|
|
161
|
+
// when what actually happened is a search that found nothing, and the
|
|
162
|
+
// commonest reason is a query in a language that storefront does not sell
|
|
163
|
+
// in. Said here rather than only in the tool description because by the time
|
|
164
|
+
// this is read the description is long out of the model's attention.
|
|
165
|
+
if (opts.products === 0 && opts.complete) {
|
|
166
|
+
lines.push(`No listings came back. This is not a finding about the category — it is a search that ` +
|
|
167
|
+
`matched nothing. Before reporting it: check the query is in the language that ` +
|
|
168
|
+
`storefront sells in, try the common shopper's word for the thing rather than a ` +
|
|
169
|
+
`category or brand-family name, and drop any quotes or operators. If a second query ` +
|
|
170
|
+
`also returns nothing, say the search found nothing rather than that the category ` +
|
|
171
|
+
`is empty.`);
|
|
172
|
+
}
|
|
173
|
+
// Before the "still running" line and before the analysis prompt: a model
|
|
174
|
+
// that has already started composing a read is past the point where a
|
|
175
|
+
// caveat changes what it writes.
|
|
176
|
+
if (opts.freshness)
|
|
177
|
+
lines.push(opts.freshness);
|
|
140
178
|
if (!opts.complete) {
|
|
141
179
|
lines.push(`Collection is still running — ${opts.pending} listing${opts.pending === 1 ? "" : "s"} to go. ` +
|
|
142
180
|
`Everything below is real and usable now; call ${statusTool} with scanId "${opts.scanId}" ` +
|
|
@@ -5,7 +5,7 @@ import { withEvidence } from "./evidence-digest.js";
|
|
|
5
5
|
import { confirmSpend, declinedResult } from "./spend.js";
|
|
6
6
|
import { allReviews, categoryGuidance, listingCount, normaliseProducts, scanCost } from "./amazon.js";
|
|
7
7
|
/**
|
|
8
|
-
* The
|
|
8
|
+
* The twelve sites, mirroring `MARKETPLACES` in nooticr-server.
|
|
9
9
|
*
|
|
10
10
|
* Kept in the same order and the same words deliberately: a model reads this
|
|
11
11
|
* text to decide what to send, and the server refuses on the same terms. Two
|
|
@@ -22,21 +22,25 @@ export const MARKETS = [
|
|
|
22
22
|
hint: "a country code or host — com (US), co.uk, de, fr, it, es, ca, co.jp, in, com.br, " +
|
|
23
23
|
"com.mx, com.au, nl, se, pl, com.be, com.tr, ae, sa, eg, sg, cn. Default www.amazon.com",
|
|
24
24
|
},
|
|
25
|
+
searchLang: "the storefront's own — English on com, co.uk, ca, in, com.au and sg, otherwise the country's",
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
28
|
slug: "aliexpress",
|
|
28
29
|
label: "AliExpress",
|
|
29
30
|
idLabel: "the numeric id from /item/<id>.html, or a full AliExpress product URL",
|
|
31
|
+
searchLang: "English",
|
|
30
32
|
},
|
|
31
33
|
{
|
|
32
34
|
slug: "cdiscount",
|
|
33
35
|
label: "Cdiscount",
|
|
34
36
|
idLabel: "the SKU from a product URL's /f-<category>-<sku>.html, or the full URL",
|
|
37
|
+
searchLang: "French",
|
|
35
38
|
},
|
|
36
39
|
{
|
|
37
40
|
slug: "flipkart",
|
|
38
41
|
label: "Flipkart",
|
|
39
42
|
idLabel: "the 16-character PID from a product URL's pid= parameter, or the full URL",
|
|
43
|
+
searchLang: "English",
|
|
40
44
|
},
|
|
41
45
|
{
|
|
42
46
|
slug: "mercadolibre",
|
|
@@ -48,6 +52,7 @@ export const MARKETS = [
|
|
|
48
52
|
"mercadolivre.com.br. Inferred from the ids when omitted, and a job runs against one " +
|
|
49
53
|
"market: an id from another is refused rather than mis-filed",
|
|
50
54
|
},
|
|
55
|
+
searchLang: "Spanish, except Brazil (MLB), which is Portuguese",
|
|
51
56
|
},
|
|
52
57
|
{
|
|
53
58
|
slug: "lazada",
|
|
@@ -59,6 +64,7 @@ export const MARKETS = [
|
|
|
59
64
|
hint: "sg, my, th, vn, ph or id. An item id is scoped to one of them — the same id on another " +
|
|
60
65
|
"Lazada host is a different product or nothing at all",
|
|
61
66
|
},
|
|
67
|
+
searchLang: "English on sg and ph; Thai, Vietnamese, Indonesian or Malay on th, vn, id and my",
|
|
62
68
|
},
|
|
63
69
|
{
|
|
64
70
|
slug: "otto",
|
|
@@ -70,6 +76,7 @@ export const MARKETS = [
|
|
|
70
76
|
hint: "de or at. The two are not interchangeable: an id from one does not address a product " +
|
|
71
77
|
"on the other",
|
|
72
78
|
},
|
|
79
|
+
searchLang: "German, on both storefronts",
|
|
73
80
|
},
|
|
74
81
|
{
|
|
75
82
|
slug: "rakuten",
|
|
@@ -81,6 +88,7 @@ export const MARKETS = [
|
|
|
81
88
|
hint: "jp (Ichiba) or tw. Taiwan publishes no review text, so a category study there reads on " +
|
|
82
89
|
"price and rating alone",
|
|
83
90
|
},
|
|
91
|
+
searchLang: "Japanese on jp, Traditional Chinese on tw",
|
|
84
92
|
},
|
|
85
93
|
{
|
|
86
94
|
slug: "trendyol",
|
|
@@ -91,6 +99,7 @@ export const MARKETS = [
|
|
|
91
99
|
hint: "one of 47 storefronts — tr, de, ae, sa, at, be, fr, it, nl, pl, ro and the rest. An id " +
|
|
92
100
|
"is scoped to one: the same number 404s on another country's",
|
|
93
101
|
},
|
|
102
|
+
searchLang: "Turkish on tr; the storefront's own language elsewhere",
|
|
94
103
|
},
|
|
95
104
|
{
|
|
96
105
|
slug: "jumia",
|
|
@@ -98,11 +107,30 @@ export const MARKETS = [
|
|
|
98
107
|
idLabel: "a SKU such as OR537EA86PWGTNAFAMZ. A product URL does NOT contain it — the number at the " +
|
|
99
108
|
"end of a product path is a different handle — so reach a product through `query` instead",
|
|
100
109
|
region: { field: "market", hint: "ng, eg, ke, ma, ci, gh, sn, tn, ug or dz" },
|
|
110
|
+
searchLang: "English on ng, ke, gh and ug; French on ci, sn, ma, tn and dz; Arabic or French on eg",
|
|
111
|
+
},
|
|
112
|
+
// The one resale site here. Its `rating` describes the seller rather than the
|
|
113
|
+
// product, because a Vinted listing is a unique second-hand item with no
|
|
114
|
+
// reviews of its own — the server says so in `ratingScope`/`ratingNote` on
|
|
115
|
+
// every scan of it, and those are worth passing on rather than summarising
|
|
116
|
+
// away.
|
|
117
|
+
{
|
|
118
|
+
slug: "vinted",
|
|
119
|
+
label: "Vinted",
|
|
120
|
+
idLabel: "the numeric id from /items/<id>, or a full Vinted item URL",
|
|
121
|
+
region: {
|
|
122
|
+
field: "market",
|
|
123
|
+
hint: "one of 27 European storefronts — fr, de, es, it, pl, co.uk, com (US), com.au and the " +
|
|
124
|
+
"rest. A listing belongs to exactly one: the same item id does not exist on another " +
|
|
125
|
+
"country's Vinted",
|
|
126
|
+
},
|
|
127
|
+
searchLang: "the storefront's own — French on fr, German on de, Polish on pl, and so on",
|
|
101
128
|
},
|
|
102
129
|
{
|
|
103
130
|
slug: "temu",
|
|
104
131
|
label: "Temu",
|
|
105
132
|
idLabel: "the goods id after -g- in a product URL, or the full URL",
|
|
133
|
+
searchLang: "English",
|
|
106
134
|
},
|
|
107
135
|
];
|
|
108
136
|
const BY_SLUG = new Map(MARKETS.map((m) => [m.slug, m]));
|
|
@@ -114,7 +142,7 @@ const SLUGS = MARKETS.map((m) => m.slug).join(", ");
|
|
|
114
142
|
/**
|
|
115
143
|
* The storefront argument, described site by site.
|
|
116
144
|
*
|
|
117
|
-
* One string rather than
|
|
145
|
+
* One string rather than twelve schemas, because a tool takes one `marketplace`
|
|
118
146
|
* per call and the host shows the whole description whichever it picks. Sites
|
|
119
147
|
* with a single storefront are named as taking none, so a model does not
|
|
120
148
|
* invent one for Temu.
|
|
@@ -124,6 +152,8 @@ const MARKET_HINT = MARKETS.filter((m) => m.region)
|
|
|
124
152
|
.join(" · ") +
|
|
125
153
|
` · ${MARKETS.filter((m) => !m.region).map((m) => m.label).join(", ")} have one storefront each ` +
|
|
126
154
|
"and take no market argument.";
|
|
155
|
+
/** Which language a query has to be in, site by site. */
|
|
156
|
+
const LANG_HINT = MARKETS.map((m) => `${m.label}: ${m.searchLang}`).join(" · ");
|
|
127
157
|
/** The product ids each site addresses by, for `items`. */
|
|
128
158
|
const ID_HINT = MARKETS.map((m) => `${m.label}: ${m.idLabel}`).join(" · ");
|
|
129
159
|
function structured(res) {
|
|
@@ -161,6 +191,9 @@ function marketplaceResult(payload, m, focus) {
|
|
|
161
191
|
scanId: String(payload.scanId ?? ""),
|
|
162
192
|
pending: Math.max(0, total - done),
|
|
163
193
|
focus,
|
|
194
|
+
// Present only when the server answered from its own store. See the field's
|
|
195
|
+
// comment in `categoryGuidance` for why it is repeated in both channels.
|
|
196
|
+
freshness: typeof payload.freshness === "string" ? payload.freshness : undefined,
|
|
164
197
|
site: m.label,
|
|
165
198
|
statusTool: "marketplace_scan_status",
|
|
166
199
|
// There is no free insights view for these sites yet, and pointing a model
|
|
@@ -177,6 +210,12 @@ function marketplaceResult(payload, m, focus) {
|
|
|
177
210
|
complete: payload.complete !== false,
|
|
178
211
|
query: payload.query ?? null,
|
|
179
212
|
market: payload.market ?? payload.domain ?? null,
|
|
213
|
+
// Where this answer came from, and when the figures in it were measured.
|
|
214
|
+
// `collectedAt` is when the result was assembled, which on a stored answer
|
|
215
|
+
// is now and says nothing about the price's age; `observedAt` is the age.
|
|
216
|
+
servedFrom: payload.servedFrom ?? "live",
|
|
217
|
+
observedAt: payload.observedAt ?? null,
|
|
218
|
+
freshness: payload.freshness ?? null,
|
|
180
219
|
progress,
|
|
181
220
|
rollup,
|
|
182
221
|
products,
|
|
@@ -197,7 +236,7 @@ export function registerMarketplaceTools(server, makeClient) {
|
|
|
197
236
|
server.registerTool("scan_marketplace_category", {
|
|
198
237
|
title: "Scan Marketplace Category",
|
|
199
238
|
_meta: viewMeta("scan_marketplace_category"),
|
|
200
|
-
description: "Collect a category from any of
|
|
239
|
+
description: "Collect a category from any of twelve marketplaces and read it: products with prices, " +
|
|
201
240
|
"star histograms, the review text itself, and the arithmetic over them (price spread, " +
|
|
202
241
|
"rating spread, which aspects recur across how many brands, and the share of each " +
|
|
203
242
|
"product's ratings sitting at 1-2 stars). `marketplace` picks the site — one of " +
|
|
@@ -222,7 +261,17 @@ export function registerMarketplaceTools(server, makeClient) {
|
|
|
222
261
|
inputSchema: z
|
|
223
262
|
.object({
|
|
224
263
|
marketplace: marketplaceArg,
|
|
225
|
-
query: z
|
|
264
|
+
query: z
|
|
265
|
+
.string()
|
|
266
|
+
.optional()
|
|
267
|
+
.describe("Category or keyword to search, e.g. 'cast iron skillet'. Two to four words, " +
|
|
268
|
+
"the common name a shopper would type rather than a catalogue description, " +
|
|
269
|
+
"and no quotes or AND/OR — none of these sites read them as operators, they " +
|
|
270
|
+
"search for them and narrow to nothing. It has to be in the storefront's " +
|
|
271
|
+
`language — ${LANG_HINT}. A query in the wrong language does not fail: it ` +
|
|
272
|
+
"returns a few imported listings, which reads as a thin category and is " +
|
|
273
|
+
"really a bad search. Brand names are not translated. A scan that came back " +
|
|
274
|
+
"with no products is a reason to change the query, not to raise the limit."),
|
|
226
275
|
items: z
|
|
227
276
|
.array(z.string())
|
|
228
277
|
.optional()
|
|
@@ -329,7 +378,7 @@ export function registerMarketplaceTools(server, makeClient) {
|
|
|
329
378
|
server.registerTool("get_marketplace_product", {
|
|
330
379
|
title: "Get Marketplace Product",
|
|
331
380
|
_meta: viewMeta("get_marketplace_product"),
|
|
332
|
-
description: "Fetch one product from any of
|
|
381
|
+
description: "Fetch one product from any of twelve marketplaces by id or URL: price, rating, the full " +
|
|
333
382
|
"star histogram, specs, and the review text. `marketplace` picks the site — one of " +
|
|
334
383
|
`${SLUGS}. ` +
|
|
335
384
|
"Costs 3 nooticr credits. " +
|
package/dist/shared/tools.js
CHANGED
|
@@ -28,7 +28,7 @@ import { registerCollabTools } from "./collab.js";
|
|
|
28
28
|
import { registerAmazonTools } from "./amazon.js";
|
|
29
29
|
import { registerMarketplaceTools } from "./marketplace.js";
|
|
30
30
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
31
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
31
|
+
export const MCP_SERVER_VERSION = "1.26.40";
|
|
32
32
|
/** MCP Apps extension identifier */
|
|
33
33
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
34
34
|
/** MIME type for MCP Apps HTML resources */
|
|
@@ -3677,6 +3677,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3677
3677
|
rakuten:{label:"Rakuten",color:"#BF0000",on:"#fff",path:"M23.277 21.3L3.939 24 .722 21.3h22.555zM7.6 19.276H3.939V0h6.052a6.653 6.653 0 0 1 6.65 6.646c0 2.234-1.108 4.204-2.799 5.418l5.418 7.211h-4.585l-4.486-5.979H7.6v5.98zm0-9.64h2.392a2.992 2.992 0 0 0 2.989-2.989 2.994 2.994 0 0 0-2.989-2.986H7.6v5.975z"},
|
|
3678
3678
|
trendyol:{label:"Trendyol",color:"#F27A1A",on:"#fff"},
|
|
3679
3679
|
jumia:{label:"Jumia",color:"#F68B1E",on:"#111"},
|
|
3680
|
+
vinted:{label:"Vinted",color:"#007782",on:"#fff"},
|
|
3680
3681
|
temu:{label:"Temu",color:"#FB7701",on:"#fff"}
|
|
3681
3682
|
};
|
|
3682
3683
|
|
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.40",
|
|
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": {
|