@nooticr/mcp 1.26.45 → 1.26.46

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 CHANGED
@@ -52,7 +52,7 @@ npx @nooticr/mcp login # one-time sign-in (Google)
52
52
 
53
53
  ## Tools
54
54
 
55
- 85 tools, grouped by what you are trying to do. Prices are in nooticr credits and
55
+ 86 tools, grouped by what you are trying to do. Prices are in nooticr credits and
56
56
  match what the server actually charges.
57
57
 
58
58
  The job tools under **Answer a question you actually have** are not
@@ -152,6 +152,7 @@ a `scanId` for the rest.
152
152
  | `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. |
153
153
  | `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. |
154
154
  | `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. Given the `scanId`, it re-reads the listings from the scan itself (free) instead of drawing whatever the model re-sends, and flags any ASIN the read cites that the scan does not contain; without one, the card says its listings were not checked. |
155
+ | `show_marketplace_category_insights` | free | The same card for the other eleven marketplaces: your read beside that site's listings and reviews, labelled with the right site, re-read from the scan by `scanId` and `marketplace` so nothing is re-typed. |
155
156
 
156
157
  ### Make something
157
158
 
@@ -217,7 +217,8 @@ export function categoryGuidance(opts) {
217
217
  }
218
218
  if (insightsTool) {
219
219
  lines.push("");
220
- lines.push("To show the result in the conversation — products, their reviews, and your read side by", `side — call ${insightsTool} with what you concluded${opts.scanId ? ` and scanId "${opts.scanId}"` : " and the scanId"}.`, "It costs nothing, and it re-reads the listings from the scan itself, so do not re-send them.");
220
+ lines.push("To show the result in the conversation — products, their reviews, and your read side by", `side — call ${insightsTool} with what you concluded${opts.scanId ? ` and scanId "${opts.scanId}"` : " and the scanId"}` +
221
+ `${Object.entries(opts.statusArgs ?? {}).map(([k, v]) => `, ${k} "${v}"`).join("")}.`, "It costs nothing, and it re-reads the listings from the scan itself, so do not re-send them.");
221
222
  }
222
223
  return lines.join("\n");
223
224
  }
@@ -260,6 +261,76 @@ export const CREDITS_PER_LISTING = 3;
260
261
  export function scanCost(args) {
261
262
  return listingCount(args) * CREDITS_PER_LISTING;
262
263
  }
264
+ /**
265
+ * The read a model writes over a category scan, shared by the Amazon and
266
+ * the eleven-marketplace insights tools (#95): one shape, so the card and the
267
+ * guidance cannot drift between sites.
268
+ */
269
+ export const insightsInputShape = {
270
+ category: z.string().describe("The category this is a read of, e.g. 'Ashwagandha supplements'."),
271
+ summary: z.string().optional().describe("The category in two or three sentences."),
272
+ drivers: z
273
+ .array(z.object({
274
+ label: z.string().describe("The driver, in a few words."),
275
+ detail: z.string().optional().describe("What the reviews actually say."),
276
+ evidence: z
277
+ .array(z.string())
278
+ .optional()
279
+ .describe("Review ids or short quotes that show it."),
280
+ strength: z
281
+ .enum(["strong", "moderate", "thin"])
282
+ .optional()
283
+ .describe("How well the collected text supports it. Say `thin` rather than overstating."),
284
+ }))
285
+ .optional()
286
+ .describe("What makes someone buy in this category."),
287
+ barriers: z
288
+ .array(z.object({
289
+ label: z.string(),
290
+ detail: z.string().optional(),
291
+ evidence: z.array(z.string()).optional(),
292
+ strength: z.enum(["strong", "moderate", "thin"]).optional(),
293
+ }))
294
+ .optional()
295
+ .describe("What stops them buying, or makes them return it."),
296
+ strengths: z
297
+ .array(z.object({
298
+ brand: z.string(),
299
+ detail: z.string(),
300
+ asin: z
301
+ .string()
302
+ .optional()
303
+ .describe("The product id this is about: an ASIN on Amazon, the site's own id elsewhere."),
304
+ }))
305
+ .optional()
306
+ .describe("What each incumbent is genuinely doing well."),
307
+ gaps: z
308
+ .array(z.object({
309
+ label: z.string(),
310
+ detail: z.string().optional(),
311
+ evidence: z.array(z.string()).optional(),
312
+ }))
313
+ .optional()
314
+ .describe("What buyers keep asking for that nobody in the set serves."),
315
+ positioning: z
316
+ .array(z.object({
317
+ angle: z.string().describe("The claim, in one line."),
318
+ who: z.string().optional().describe("Who it is for."),
319
+ why: z.string().optional().describe("The driver it answers or the gap it fills."),
320
+ risk: z.string().optional().describe("What would have to be true, or what could go wrong."),
321
+ }))
322
+ .optional()
323
+ .describe("Concrete angles a new entrant could take."),
324
+ products: z
325
+ .array(anyObject())
326
+ .optional()
327
+ .describe("Only when there is no scanId. With one, the listings are re-read from the scan and anything here is ignored."),
328
+ rollup: anyObject().optional().describe("Only when there is no scanId; re-read from the scan otherwise."),
329
+ scanId: z
330
+ .string()
331
+ .optional()
332
+ .describe("The scan this reads. Pass it: it is what lets the card draw the collector's own listings."),
333
+ };
263
334
  export function registerAmazonTools(server, makeClient) {
264
335
  server.registerTool("scan_amazon_category", {
265
336
  title: "Scan Amazon Category",
@@ -454,105 +525,8 @@ export function registerAmazonTools(server, makeClient) {
454
525
  openWorldHint: false,
455
526
  },
456
527
  outputSchema: OUTPUT_SCHEMAS.show_amazon_category_insights,
457
- inputSchema: z
458
- .object({
459
- category: z.string().describe("The category this is a read of, e.g. 'Ashwagandha supplements'."),
460
- summary: z.string().optional().describe("The category in two or three sentences."),
461
- drivers: z
462
- .array(z.object({
463
- label: z.string().describe("The driver, in a few words."),
464
- detail: z.string().optional().describe("What the reviews actually say."),
465
- evidence: z
466
- .array(z.string())
467
- .optional()
468
- .describe("Review ids or short quotes that show it."),
469
- strength: z
470
- .enum(["strong", "moderate", "thin"])
471
- .optional()
472
- .describe("How well the collected text supports it. Say `thin` rather than overstating."),
473
- }))
474
- .optional()
475
- .describe("What makes someone buy in this category."),
476
- barriers: z
477
- .array(z.object({
478
- label: z.string(),
479
- detail: z.string().optional(),
480
- evidence: z.array(z.string()).optional(),
481
- strength: z.enum(["strong", "moderate", "thin"]).optional(),
482
- }))
483
- .optional()
484
- .describe("What stops them buying, or makes them return it."),
485
- strengths: z
486
- .array(z.object({
487
- brand: z.string(),
488
- detail: z.string(),
489
- asin: z.string().optional(),
490
- }))
491
- .optional()
492
- .describe("What each incumbent is genuinely doing well."),
493
- gaps: z
494
- .array(z.object({
495
- label: z.string(),
496
- detail: z.string().optional(),
497
- evidence: z.array(z.string()).optional(),
498
- }))
499
- .optional()
500
- .describe("What buyers keep asking for that nobody in the set serves."),
501
- positioning: z
502
- .array(z.object({
503
- angle: z.string().describe("The claim, in one line."),
504
- who: z.string().optional().describe("Who it is for."),
505
- why: z.string().optional().describe("The driver it answers or the gap it fills."),
506
- risk: z.string().optional().describe("What would have to be true, or what could go wrong."),
507
- }))
508
- .optional()
509
- .describe("Concrete angles a new entrant could take."),
510
- products: z
511
- .array(anyObject())
512
- .optional()
513
- .describe("Only when there is no scanId. With one, the listings are re-read from the scan and anything here is ignored."),
514
- rollup: anyObject().optional().describe("Only when there is no scanId; re-read from the scan otherwise."),
515
- scanId: z
516
- .string()
517
- .optional()
518
- .describe("The scan this reads. Pass it: it is what lets the card draw the collector's own listings."),
519
- })
520
- .passthrough(),
521
- }, async (args, extra) => {
522
- const checked = await rehydrate(args, () => makeClient({ ...extra, arguments: args }));
523
- const products = checked.products;
524
- const counts = {
525
- drivers: Array.isArray(args.drivers) ? args.drivers.length : 0,
526
- barriers: Array.isArray(args.barriers) ? args.barriers.length : 0,
527
- gaps: Array.isArray(args.gaps) ? args.gaps.length : 0,
528
- positioning: Array.isArray(args.positioning) ? args.positioning.length : 0,
529
- };
530
- const text = `Category read for ${String(args.category ?? "this category")}: ${counts.drivers} driver(s), ` +
531
- `${counts.barriers} barrier(s), ${counts.gaps} gap(s), ${counts.positioning} positioning angle(s)` +
532
- `${products.length ? `, drawn beside ${products.length} listing(s)` : ""}.` +
533
- (args.summary ? ` ${String(args.summary)}` : "") +
534
- ` ${checked.verification.note}`;
535
- return {
536
- content: [{ type: "text", text }],
537
- structuredContent: {
538
- marketplace: "amazon",
539
- view: "insights",
540
- category: String(args.category ?? ""),
541
- summary: args.summary ?? null,
542
- drivers: args.drivers ?? [],
543
- barriers: args.barriers ?? [],
544
- strengths: args.strengths ?? [],
545
- gaps: args.gaps ?? [],
546
- positioning: args.positioning ?? [],
547
- products,
548
- rollup: checked.rollup,
549
- scanId: args.scanId ?? null,
550
- verification: checked.verification,
551
- // The one read is amazon_scan_status, which is free by design.
552
- mcpCredits: { cost: 0 },
553
- },
554
- };
555
- });
528
+ inputSchema: z.object(insightsInputShape).passthrough(),
529
+ }, async (args, extra) => insightsResult(args, () => makeClient({ ...extra, arguments: args }), { marketplace: "amazon" }));
556
530
  }
557
531
  /**
558
532
  * The listings a category read is drawn beside, from the scan rather than
@@ -567,7 +541,7 @@ export function registerAmazonTools(server, makeClient) {
567
541
  * model sent is drawn and marked unverified rather than refused: a card with a
568
542
  * visible caveat is more use to a person than no card.
569
543
  */
570
- export async function rehydrate(args, client) {
544
+ export async function rehydrate(args, client, opts = {}) {
571
545
  const sent = Array.isArray(args.products) ? args.products : [];
572
546
  const sentRollup = (args.rollup ?? {});
573
547
  const scanId = typeof args.scanId === "string" ? args.scanId.trim() : "";
@@ -583,7 +557,11 @@ export async function rehydrate(args, client) {
583
557
  }
584
558
  let payload;
585
559
  try {
586
- const res = await (await client()).callTool("amazon_scan_status", { scanId, waitSeconds: 0 });
560
+ const res = await (await client()).callTool(opts.statusTool ?? "amazon_scan_status", {
561
+ ...(opts.statusArgs ?? {}),
562
+ scanId,
563
+ waitSeconds: 0,
564
+ });
587
565
  payload = structured(res);
588
566
  }
589
567
  catch (err) {
@@ -601,7 +579,7 @@ export async function rehydrate(args, client) {
601
579
  const scanComplete = payload.complete !== false;
602
580
  const flags = [
603
581
  notInScan.length ? `${notInScan.length} listing(s) you sent are not in the scan (${notInScan.join(", ")}) and are not drawn.` : "",
604
- unknownAsins.length ? `ASIN(s) cited in strengths but not in the scan: ${unknownAsins.join(", ")}.` : "",
582
+ unknownAsins.length ? `Product id(s) cited in strengths but not in the scan: ${unknownAsins.join(", ")}.` : "",
605
583
  scanComplete ? "" : "The scan is still running, so the set drawn is what it had collected so far.",
606
584
  ].filter(Boolean);
607
585
  return {
@@ -616,6 +594,48 @@ export async function rehydrate(args, client) {
616
594
  },
617
595
  };
618
596
  }
597
+ /**
598
+ * The insights card for any site: the model's read, drawn beside the scan's
599
+ * own listings, re-read by scanId so a mis-copied price or an invented
600
+ * competitor cannot reach the card (#107). `marketplace` is what the view
601
+ * labels the header with, so a Lazada read is never drawn as an Amazon one.
602
+ */
603
+ export async function insightsResult(args, client, site) {
604
+ const checked = await rehydrate(args, client, { statusTool: site.statusTool, statusArgs: site.statusArgs });
605
+ const products = checked.products;
606
+ const counts = {
607
+ drivers: Array.isArray(args.drivers) ? args.drivers.length : 0,
608
+ barriers: Array.isArray(args.barriers) ? args.barriers.length : 0,
609
+ gaps: Array.isArray(args.gaps) ? args.gaps.length : 0,
610
+ positioning: Array.isArray(args.positioning) ? args.positioning.length : 0,
611
+ };
612
+ const on = site.label ? ` on ${site.label}` : "";
613
+ const text = `Category read for ${String(args.category ?? "this category")}${on}: ${counts.drivers} driver(s), ` +
614
+ `${counts.barriers} barrier(s), ${counts.gaps} gap(s), ${counts.positioning} positioning angle(s)` +
615
+ `${products.length ? `, drawn beside ${products.length} listing(s)` : ""}.` +
616
+ (args.summary ? ` ${String(args.summary)}` : "") +
617
+ ` ${checked.verification.note}`;
618
+ return {
619
+ content: [{ type: "text", text }],
620
+ structuredContent: {
621
+ marketplace: site.marketplace,
622
+ view: "insights",
623
+ category: String(args.category ?? ""),
624
+ summary: args.summary ?? null,
625
+ drivers: args.drivers ?? [],
626
+ barriers: args.barriers ?? [],
627
+ strengths: args.strengths ?? [],
628
+ gaps: args.gaps ?? [],
629
+ positioning: args.positioning ?? [],
630
+ products,
631
+ rollup: checked.rollup,
632
+ scanId: args.scanId ?? null,
633
+ verification: checked.verification,
634
+ // The one read is the site's scan-status poll, free by design.
635
+ mcpCredits: { cost: 0 },
636
+ },
637
+ };
638
+ }
619
639
  /**
620
640
  * One scan result, shaped for both channels.
621
641
  *
@@ -353,6 +353,13 @@ export const LOADING_PLANS = {
353
353
  steps: [],
354
354
  free: true,
355
355
  },
356
+ show_marketplace_category_insights: {
357
+ label: "Drawing the category read",
358
+ kind: "list",
359
+ n: 5,
360
+ steps: [],
361
+ free: true,
362
+ },
356
363
  // ─── Marketplace ───
357
364
  //
358
365
  // The wait here is genuinely long and the reason is worth showing rather
@@ -3,7 +3,7 @@ import { OUTPUT_SCHEMAS } from "./output-schemas.js";
3
3
  import { viewMeta } from "./view-meta.js";
4
4
  import { withEvidence } from "./evidence-digest.js";
5
5
  import { confirmSpend, declinedResult } from "./spend.js";
6
- import { allReviews, categoryGuidance, listingCount, nextPoll, normaliseProducts, pollTail, scanCost } from "./amazon.js";
6
+ import { allReviews, categoryGuidance, insightsInputShape, insightsResult, listingCount, nextPoll, normaliseProducts, pollTail, scanCost, } from "./amazon.js";
7
7
  /**
8
8
  * The twelve sites, mirroring `MARKETPLACES` in nooticr-server.
9
9
  *
@@ -199,10 +199,10 @@ function marketplaceResult(payload, m, focus) {
199
199
  statusArgs: { marketplace: m.slug },
200
200
  status: typeof payload.status === "string" ? payload.status : undefined,
201
201
  total: num(progress.total) ?? undefined,
202
- // There is no free insights view for these sites yet, and pointing a model
203
- // at show_amazon_category_insights would have it draw a Lazada scan as an
204
- // Amazon one. Better to say nothing than to name the wrong tool.
205
- insightsTool: null,
202
+ // Its own view, labelled with this site (#95): pointing a model at
203
+ // show_amazon_category_insights would have it draw a Lazada scan as an
204
+ // Amazon one.
205
+ insightsTool: "show_marketplace_category_insights",
206
206
  });
207
207
  const structuredPayload = {
208
208
  guidance,
@@ -426,4 +426,48 @@ export function registerMarketplaceTools(server, makeClient) {
426
426
  return toolError("get_marketplace_product failed", err);
427
427
  }
428
428
  });
429
+ /**
430
+ * The read a model writes from scan_marketplace_category, drawn beside the
431
+ * site's own listings (#95). The same card and the same check against the
432
+ * scan as show_amazon_category_insights; the name shares whole words with
433
+ * its predecessor (`marketplace`, `category`) because that is what decides
434
+ * whether a host's tool search returns it.
435
+ */
436
+ server.registerTool("show_marketplace_category_insights", {
437
+ title: "Show Marketplace Category Insights",
438
+ _meta: viewMeta("show_marketplace_category_insights"),
439
+ description: "Display the category read you produced from scan_marketplace_category: purchase drivers, " +
440
+ "barriers, what each brand does well, the gaps and the positioning angles, drawn beside the " +
441
+ "site's listings and their reviews so a person can check any claim against the text it came " +
442
+ "from. Pass the same marketplace and the scanId: the listings are then re-read from the scan " +
443
+ "itself (free) rather than taken from you, so do not re-send them, and any product id you " +
444
+ "cite that the scan does not contain is flagged. Without a scanId, listings you pass are " +
445
+ "drawn marked as not checked against a scan. Free. Your read is attributed to you, never " +
446
+ "presented as a nooticr rating of anyone's product. For Amazon scans use " +
447
+ "show_amazon_category_insights. Call this after you have read the reviews, not instead of reading them.",
448
+ annotations: {
449
+ readOnlyHint: true,
450
+ destructiveHint: false,
451
+ idempotentHint: true,
452
+ // Its one request re-reads a scan the caller already ran, from
453
+ // nooticr's job store with no wait: nothing new is fetched.
454
+ openWorldHint: false,
455
+ },
456
+ outputSchema: OUTPUT_SCHEMAS.show_marketplace_category_insights,
457
+ inputSchema: z.object({ marketplace: marketplaceArg, ...insightsInputShape }).passthrough(),
458
+ }, async (args, extra) => {
459
+ const m = BY_SLUG.get(String(args.marketplace ?? ""));
460
+ if (!m) {
461
+ return {
462
+ content: [{ type: "text", text: `Unknown marketplace "${String(args.marketplace)}". One of: ${SLUGS}.` }],
463
+ isError: true,
464
+ };
465
+ }
466
+ return insightsResult(args, () => makeClient({ ...extra, arguments: args }), {
467
+ marketplace: m.slug,
468
+ label: m.label,
469
+ statusTool: "marketplace_scan_status",
470
+ statusArgs: { marketplace: m.slug },
471
+ });
472
+ });
429
473
  }
@@ -311,6 +311,29 @@ const marketplaceScanShape = {
311
311
  };
312
312
  const marketplaceScan = open(marketplaceScanShape);
313
313
  const amazonScan = open(amazonScanShape);
314
+ /** A category read drawn beside a scan's listings, on any site. */
315
+ const categoryInsights = open({
316
+ marketplace: scalar(),
317
+ view: scalar(),
318
+ category: scalar(),
319
+ summary: scalar(),
320
+ drivers: listOf(amazonFinding).describe("What makes someone buy, as you read it."),
321
+ barriers: listOf(amazonFinding).describe("What stops them, or makes them return it."),
322
+ strengths: listOf(open({ brand: scalar(), detail: scalar(), asin: scalar() })),
323
+ gaps: listOf(amazonFinding),
324
+ positioning: listOf(open({ angle: scalar(), who: scalar(), why: scalar(), risk: scalar() })),
325
+ products: listOf(amazonProduct).describe("The listings drawn: the scan's own, re-read by scanId, unless verification.status is unverified."),
326
+ rollup: open({}).nullish(),
327
+ scanId: scalar(),
328
+ verification: open({
329
+ status: scalar().describe("verified: the listings are the scan's own. unverified: as re-sent by the model."),
330
+ note: scalar(),
331
+ scanComplete: scalar(),
332
+ notInScan: listOf(z.string()).describe("Listings the model sent that the scan does not contain; not drawn."),
333
+ unknownAsins: listOf(z.string()).describe("ASINs cited in strengths that the scan does not contain."),
334
+ }).nullish(),
335
+ mcpCredits,
336
+ });
314
337
  /**
315
338
  * Whether a show_* view drew nooticr's own figures (#107): the session's
316
339
  * fetch ledger overlays what was actually returned on what the model sent.
@@ -1556,26 +1579,7 @@ export const OUTPUT_SCHEMAS = {
1556
1579
  ...amazonScanShape,
1557
1580
  product: amazonProduct.nullish().describe("The single listing, also present as products[0]."),
1558
1581
  }),
1559
- show_amazon_category_insights: open({
1560
- marketplace: scalar(),
1561
- view: scalar(),
1562
- category: scalar(),
1563
- summary: scalar(),
1564
- drivers: listOf(amazonFinding).describe("What makes someone buy, as you read it."),
1565
- barriers: listOf(amazonFinding).describe("What stops them, or makes them return it."),
1566
- strengths: listOf(open({ brand: scalar(), detail: scalar(), asin: scalar() })),
1567
- gaps: listOf(amazonFinding),
1568
- positioning: listOf(open({ angle: scalar(), who: scalar(), why: scalar(), risk: scalar() })),
1569
- products: listOf(amazonProduct).describe("The listings drawn: the scan's own, re-read by scanId, unless verification.status is unverified."),
1570
- rollup: open({}).nullish(),
1571
- scanId: scalar(),
1572
- verification: open({
1573
- status: scalar().describe("verified: the listings are the scan's own. unverified: as re-sent by the model."),
1574
- note: scalar(),
1575
- scanComplete: scalar(),
1576
- notInScan: listOf(z.string()).describe("Listings the model sent that the scan does not contain; not drawn."),
1577
- unknownAsins: listOf(z.string()).describe("ASINs cited in strengths that the scan does not contain."),
1578
- }).nullish(),
1579
- mcpCredits,
1580
- }),
1582
+ show_amazon_category_insights: categoryInsights,
1583
+ // The same card for the other eleven sites (#95); `marketplace` says which.
1584
+ show_marketplace_category_insights: categoryInsights,
1581
1585
  };
@@ -510,4 +510,10 @@ export const TOOL_DEFINITIONS = [
510
510
  description: "Display the category read you produced from scan_amazon_category: purchase drivers, barriers, what each brand does well, the gaps, and the positioning angles \u2014 drawn beside the listings and their reviews, so a person can click a product and check any claim against the text it came from. Pass the scanId: the listings are then re-read from the scan itself (free) rather than taken from you, and any ASIN you cite that the scan does not contain is flagged. Without a scanId, listings you pass are drawn marked as not checked against a scan. Free. Your read is attributed to you rather than presented as a nooticr rating of anyone's product. Call this after you have read the reviews, not instead of reading them.",
511
511
  inputSchema: z.object({ category: z.string().describe("The category this is a read of."), summary: z.string().optional(), drivers: z.array(z.any()).optional().describe("What makes someone buy, each with the review evidence behind it."), barriers: z.array(z.any()).optional().describe("What stops them buying, or makes them return it."), strengths: z.array(z.any()).optional().describe("What each incumbent does well."), gaps: z.array(z.any()).optional().describe("What buyers keep asking for that nobody serves."), positioning: z.array(z.any()).optional().describe("Angles a new entrant could take."), products: z.array(z.any()).optional().describe("The listings from the scan, passed straight through."), rollup: z.record(z.unknown()).optional().describe("The rollup from the scan, passed straight through."), scanId: z.string().optional() }).passthrough(),
512
512
  },
513
+ {
514
+ name: "show_marketplace_category_insights",
515
+ title: "Show Marketplace Category Insights",
516
+ description: "Display the category read you produced from scan_marketplace_category: purchase drivers, barriers, what each brand does well, the gaps, and the positioning angles \u2014 drawn beside the listings and their reviews, so a person can click a product and check any claim against the text it came from. Pass the scanId: the listings are then re-read from the scan itself (free) rather than taken from you, and any ASIN you cite that the scan does not contain is flagged. Without a scanId, listings you pass are drawn marked as not checked against a scan. Free. Your read is attributed to you rather than presented as a nooticr rating of anyone's product. Call this after you have read the reviews, not instead of reading them.",
517
+ inputSchema: z.object({ marketplace: z.string().describe("The site the scan ran on, e.g. lazada."), category: z.string().describe("The category this is a read of."), summary: z.string().optional(), drivers: z.array(z.any()).optional().describe("What makes someone buy, each with the review evidence behind it."), barriers: z.array(z.any()).optional().describe("What stops them buying, or makes them return it."), strengths: z.array(z.any()).optional().describe("What each incumbent does well."), gaps: z.array(z.any()).optional().describe("What buyers keep asking for that nobody serves."), positioning: z.array(z.any()).optional().describe("Angles a new entrant could take."), products: z.array(z.any()).optional().describe("The listings from the scan, passed straight through."), rollup: z.record(z.unknown()).optional().describe("The rollup from the scan, passed straight through."), scanId: z.string().optional() }).passthrough(),
518
+ },
513
519
  ];
@@ -30,7 +30,7 @@ import { registerCollabTools } from "./collab.js";
30
30
  import { registerAmazonTools } from "./amazon.js";
31
31
  import { registerMarketplaceTools } from "./marketplace.js";
32
32
  /** Current MCP server version — bumped on every deploy for traceability. */
33
- export const MCP_SERVER_VERSION = "1.26.45";
33
+ export const MCP_SERVER_VERSION = "1.26.46";
34
34
  /** MCP Apps extension identifier */
35
35
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
36
36
  /** MIME type for MCP Apps HTML resources */
@@ -694,6 +694,7 @@ export function createMcpServer(rawMakeClient, opts) {
694
694
  "amazon_scan_status",
695
695
  "get_amazon_product",
696
696
  "show_amazon_category_insights",
697
+ "show_marketplace_category_insights",
697
698
  // The marketplace-agnostic trio. Every name here gets a ui://nooticr/<tool>
698
699
  // resource, which is what gives a tool its card — a tool missing from this
699
700
  // list is one the host is told to render and then 404s on.
@@ -66,7 +66,8 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
66
66
  get_amazon_product:"Get Amazon Product",
67
67
  scan_marketplace_category:"Scan Marketplace Category",
68
68
  marketplace_scan_status:"Marketplace Scan Status",
69
- get_marketplace_product:"Get Marketplace Product"
69
+ get_marketplace_product:"Get Marketplace Product",
70
+ show_marketplace_category_insights:"Marketplace Category Insights"
70
71
  };
71
72
  var TOOL_DESCS={
72
73
  analyze_post:"Frames from the post plus its transcript, for you to read yourself.",
@@ -102,9 +103,10 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
102
103
  scan_amazon_category:"A category on Amazon — prices, ratings and what reviewers keep raising.",
103
104
  amazon_scan_status:"The listings an Amazon scan has collected so far.",
104
105
  get_amazon_product:"One Amazon listing: price, rating, star histogram and reviews.",
105
- scan_marketplace_category:"A category on any of eleven marketplaces — prices, ratings and review themes.",
106
+ scan_marketplace_category:"A category on any of twelve marketplaces — prices, ratings and review themes.",
106
107
  marketplace_scan_status:"The products a marketplace scan has collected so far.",
107
- get_marketplace_product:"One product from any marketplace: price, rating, histogram and reviews."
108
+ get_marketplace_product:"One product from any marketplace: price, rating, histogram and reviews.",
109
+ show_marketplace_category_insights:"Your read of a marketplace category, beside its listings."
108
110
  };
109
111
 
110
112
  // ─── The mark ───
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.45",
4
+ "version": "1.26.46",
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": {