@nooticr/mcp 1.26.37 → 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/dist/shared/amazon.js +16 -3
- package/dist/shared/tools.js +1 -1
- package/dist/shared/ui-template.js +56 -156
- package/package.json +1 -1
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),
|
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.38";
|
|
32
32
|
/** MCP Apps extension identifier */
|
|
33
33
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
34
34
|
/** MIME type for MCP Apps HTML resources */
|
|
@@ -3616,16 +3616,59 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3616
3616
|
+'<path fill="#FF9900" d="'+d+'"></path></svg>';
|
|
3617
3617
|
}
|
|
3618
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
|
+
|
|
3619
3649
|
/** The mark for whichever marketplace a payload came from. */
|
|
3620
3650
|
function marketplaceMark(name,size){
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
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>";
|
|
3624
3660
|
}
|
|
3625
3661
|
|
|
3626
|
-
/**
|
|
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
|
+
*/
|
|
3627
3669
|
function marketDomain(domain){
|
|
3628
|
-
var d=String(domain||"
|
|
3670
|
+
var d=String(domain||"");
|
|
3671
|
+
if(!d)return "";
|
|
3629
3672
|
return d.indexOf("www.")===0?d.slice(4):d;
|
|
3630
3673
|
}
|
|
3631
3674
|
|
|
@@ -3713,7 +3756,9 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3713
3756
|
var open=amazonState.open===String(p.asin||"");
|
|
3714
3757
|
var neg=null;
|
|
3715
3758
|
(((amazonState.rollup||{}).perProduct)||[]).forEach(function(r){
|
|
3716
|
-
|
|
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;
|
|
3717
3762
|
});
|
|
3718
3763
|
return '<button type="button" class="amz-tile'+(open?" amz-tile-open":"")+'" data-amz-open="'+esc(p.asin||"")+'">'
|
|
3719
3764
|
+productThumb(p)
|
|
@@ -4033,7 +4078,10 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4033
4078
|
:((insights&&amazonState)?(amazonState.rollup||{}):{});
|
|
4034
4079
|
amazonState={
|
|
4035
4080
|
marketplace:String(d.marketplace||"amazon"),
|
|
4036
|
-
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)||""),
|
|
4037
4085
|
title:insights?String(d.category||"Category read"):String(d.query||"Category scan"),
|
|
4038
4086
|
products:carried,
|
|
4039
4087
|
rollup:rollup,
|
|
@@ -4049,148 +4097,6 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4049
4097
|
renderAmazon();
|
|
4050
4098
|
}
|
|
4051
4099
|
|
|
4052
|
-
/* ------------------------------------------------------------------ */
|
|
4053
|
-
/* Marketplaces */
|
|
4054
|
-
/* ------------------------------------------------------------------ */
|
|
4055
|
-
|
|
4056
|
-
/* Eleven sites reach these tools through one contract, so one view draws
|
|
4057
|
-
them all; the slug only decides what the header is called. Before this
|
|
4058
|
-
every marketplace result fell through to the JSON dump at the bottom of
|
|
4059
|
-
renderView — the tools were callable from Claude and ChatGPT and had no
|
|
4060
|
-
interactive view at all. */
|
|
4061
|
-
var MARKET_LABELS={amazon:"Amazon",aliexpress:"AliExpress",cdiscount:"Cdiscount",
|
|
4062
|
-
flipkart:"Flipkart",mercadolibre:"Mercado Libre",lazada:"Lazada",otto:"Otto",
|
|
4063
|
-
rakuten:"Rakuten",trendyol:"Trendyol",jumia:"Jumia",temu:"Temu"};
|
|
4064
|
-
function mLabel(slug){return MARKET_LABELS[String(slug||"").toLowerCase()]||(slug?String(slug):"Marketplace");}
|
|
4065
|
-
|
|
4066
|
-
/* The raw scraper record is snake_case and the rollup's copy is camelCase.
|
|
4067
|
-
Both reach this view, so both spellings are read rather than picking one
|
|
4068
|
-
and quietly dropping half the fields. */
|
|
4069
|
-
function mGet(p,a,b){var v=p[a];if(v===undefined||v===null)v=p[b];return v;}
|
|
4070
|
-
|
|
4071
|
-
/* Display price. The "price" field is already formatted by the collector
|
|
4072
|
-
("EUR 19.99",
|
|
4073
|
-
"19,99 TL"), so it wins; the number is only formatted when there is no
|
|
4074
|
-
string. A product whose price was not scraped says so rather than
|
|
4075
|
-
printing a zero, which reads as free. */
|
|
4076
|
-
function mPrice(p){
|
|
4077
|
-
var s=mGet(p,"price","price");
|
|
4078
|
-
if(typeof s==="string"&&s.trim())return s.trim();
|
|
4079
|
-
var v=mGet(p,"price_value","priceValue");
|
|
4080
|
-
if(v===undefined||v===null||!isFinite(Number(v)))return "";
|
|
4081
|
-
var c=mGet(p,"currency","currency")||"";
|
|
4082
|
-
return (c?c+" ":"")+Number(v).toFixed(2);
|
|
4083
|
-
}
|
|
4084
|
-
|
|
4085
|
-
function mStars(r){
|
|
4086
|
-
r=Number(r)||0;
|
|
4087
|
-
if(!r)return "";
|
|
4088
|
-
return '<span style="color:var(--amber,#F59E0B);font-weight:700">'+r.toFixed(1)+"</span>";
|
|
4089
|
-
}
|
|
4090
|
-
|
|
4091
|
-
/* One product tile. The image is the reason this view exists rather than a
|
|
4092
|
-
table: a category reads as a shelf, and the collectors already return
|
|
4093
|
-
"images". */
|
|
4094
|
-
function mTile(p){
|
|
4095
|
-
var imgs=p.images;
|
|
4096
|
-
var img=(Array.isArray(imgs)&&imgs.length?imgs[0]:"")||p.image||p.imageUrl||p.image_url||"";
|
|
4097
|
-
var title=String(p.title||p.name||"").slice(0,120);
|
|
4098
|
-
var price=mPrice(p);
|
|
4099
|
-
var rating=Number(mGet(p,"rating","rating"))||0;
|
|
4100
|
-
var count=Number(mGet(p,"rating_count","ratingCount"))||0;
|
|
4101
|
-
var url=String(p.url||"");
|
|
4102
|
-
var seller=String(mGet(p,"brand","seller")||mGet(p,"seller","brand")||"");
|
|
4103
|
-
var media=img
|
|
4104
|
-
? '<img src="'+esc(img)+'" alt="" loading="lazy" referrerpolicy="no-referrer" style="width:100%;height:110px;object-fit:contain;background:var(--tag);display:block"/>'
|
|
4105
|
-
: '<div style="width:100%;height:110px;background:var(--tag)"></div>';
|
|
4106
|
-
var body='<div style="padding:8px 9px">'
|
|
4107
|
-
+'<div style="font-size:12px;line-height:1.35;font-weight:600;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;min-height:2.7em">'+esc(title)+"</div>"
|
|
4108
|
-
+(seller?'<div style="font-size:11px;color:var(--muted);margin-top:3px">'+esc(seller)+"</div>":"")
|
|
4109
|
-
+'<div style="display:flex;align-items:baseline;gap:6px;margin-top:6px">'
|
|
4110
|
-
+(price?'<span style="font-size:13.5px;font-weight:700">'+esc(price)+"</span>":'<span style="font-size:12px;color:var(--muted)">no price</span>')
|
|
4111
|
-
+"</div>"
|
|
4112
|
-
+(rating?'<div style="font-size:11.5px;color:var(--muted);margin-top:3px">'+mStars(rating)+(count?" · "+fmtNum(count)+" ratings":"")+"</div>":"")
|
|
4113
|
-
+"</div>";
|
|
4114
|
-
var inner=media+body;
|
|
4115
|
-
return '<div style="border:1px solid var(--border);border-radius:10px;overflow:hidden;background:var(--bg-surface,transparent)">'
|
|
4116
|
-
+(url?'<a href="'+esc(url)+'" target="_blank" rel="noopener noreferrer" style="color:inherit;text-decoration:none;display:block">'+inner+"</a>":inner)
|
|
4117
|
-
+"</div>";
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4120
|
-
/* An aspect is coloured by whichever way its brands actually lean. A chip
|
|
4121
|
-
with no lean stays neutral rather than being guessed at, for the same
|
|
4122
|
-
reason the monitor view refuses a sentiment badge. */
|
|
4123
|
-
function mAspect(a){
|
|
4124
|
-
var pos=Number(a.positiveBrands)||0,neg=Number(a.negativeBrands)||0;
|
|
4125
|
-
var cls=pos>neg?"chip-positive":neg>pos?"chip-negative":"chip-neutral";
|
|
4126
|
-
return '<span class="chip '+cls+'">'+esc(String(a.name||""))+' <b>'+fmtNum(a.mentions||0)+"</b></span>";
|
|
4127
|
-
}
|
|
4128
|
-
|
|
4129
|
-
function mStat(label,value){
|
|
4130
|
-
if(value===""||value===undefined||value===null)return "";
|
|
4131
|
-
return '<div><div style="font-size:10.5px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">'+esc(label)+"</div>"
|
|
4132
|
-
+'<div style="font-size:15px;font-weight:700;margin-top:2px">'+value+"</div></div>";
|
|
4133
|
-
}
|
|
4134
|
-
|
|
4135
|
-
function marketCard(d){
|
|
4136
|
-
var r=d.rollup||{};
|
|
4137
|
-
var single=d.product&&!Array.isArray(d.products&&d.products.length>1?d.products:null);
|
|
4138
|
-
/* Prefer the raw records: they carry the images, and the rollup's copy of
|
|
4139
|
-
each product exists for the arithmetic rather than for drawing. */
|
|
4140
|
-
var rows=(Array.isArray(d.products)&&d.products.length?d.products:(r.perProduct||[])).slice(0,24);
|
|
4141
|
-
var running=d.complete===false;
|
|
4142
|
-
var prog=d.progress||{};
|
|
4143
|
-
var done=Number(prog.done)||0,total=Number(prog.total)||0;
|
|
4144
|
-
var pct=total>0?Math.min(100,Math.round(done/total*100)):0;
|
|
4145
|
-
|
|
4146
|
-
var head='<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">'
|
|
4147
|
-
+'<span style="font-size:16px;font-weight:700">'+esc(mLabel(d.marketplace))+"</span>"
|
|
4148
|
-
+(d.query?'<span style="font-size:13px;color:var(--muted)">'+esc(String(d.query))+"</span>":"")
|
|
4149
|
-
+(d.market?'<span class="chip chip-neutral">'+esc(String(d.market))+"</span>":"")
|
|
4150
|
-
+(running?'<span class="chip chip-neutral">collecting</span>':"")
|
|
4151
|
-
+"</div>";
|
|
4152
|
-
|
|
4153
|
-
var bar=running&&total>0
|
|
4154
|
-
? '<div style="margin-top:8px"><div style="height:4px;border-radius:999px;background:var(--tag);overflow:hidden">'
|
|
4155
|
-
+'<div style="height:100%;width:'+pct+'%;background:var(--green,#16A34A)"></div></div>'
|
|
4156
|
-
+'<div style="font-size:11.5px;color:var(--muted);margin-top:4px">'+done+" of "+total
|
|
4157
|
-
+(prog.message?" · "+esc(String(prog.message)):"")+"</div></div>"
|
|
4158
|
-
: "";
|
|
4159
|
-
|
|
4160
|
-
var price=r.price||{},rating=r.rating||{};
|
|
4161
|
-
var money=function(v){return (v===undefined||v===null)?"":Number(v).toFixed(2);};
|
|
4162
|
-
var stats='<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:12px">'
|
|
4163
|
-
+mStat("Products",fmtNum(r.products||rows.length||0))
|
|
4164
|
-
+mStat("Median price",money(price.median))
|
|
4165
|
-
+mStat("Rating",rating.mean?Number(rating.mean).toFixed(2):"")
|
|
4166
|
-
+mStat("Ratings behind it",r.ratingsRepresented?fmtNum(r.ratingsRepresented):"")
|
|
4167
|
-
+"</div>";
|
|
4168
|
-
|
|
4169
|
-
/* Both numbers, because they are not the same claim: a scrape reads a few
|
|
4170
|
-
dozen review texts while the stars average tens of thousands, and
|
|
4171
|
-
showing only the first invites reading it as the category's whole
|
|
4172
|
-
voice. */
|
|
4173
|
-
var reviews=r.reviewsCollected
|
|
4174
|
-
? '<div style="font-size:11.5px;color:var(--muted);margin-top:8px">'
|
|
4175
|
-
+fmtNum(r.reviewsCollected)+" review texts read"
|
|
4176
|
-
+(r.reviewsGatedProducts?" · "+r.reviewsGatedProducts+" products gated":"")+"</div>"
|
|
4177
|
-
: "";
|
|
4178
|
-
|
|
4179
|
-
var aspects=(r.aspects||[]).slice(0,10);
|
|
4180
|
-
var aspectHtml=aspects.length
|
|
4181
|
-
? '<div style="margin-top:14px"><div class="sec-label">What reviewers raise</div>'
|
|
4182
|
-
+'<div class="chiprow">'+aspects.map(mAspect).join("")+"</div></div>"
|
|
4183
|
-
: "";
|
|
4184
|
-
|
|
4185
|
-
var grid=rows.length
|
|
4186
|
-
? '<div style="margin-top:14px"><div class="sec-label">'+(single?"Product":"Products")+"</div>"
|
|
4187
|
-
+'<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:10px">'
|
|
4188
|
-
+rows.map(mTile).join("")+"</div></div>"
|
|
4189
|
-
: '<div style="font-size:12.5px;color:var(--muted);margin-top:12px">Nothing collected yet.</div>';
|
|
4190
|
-
|
|
4191
|
-
return '<div class="card card-wide fade-in"><div class="card-body">'+head+bar+stats+reviews+aspectHtml+grid+"</div></div>";
|
|
4192
|
-
}
|
|
4193
|
-
|
|
4194
4100
|
function renderView(result){
|
|
4195
4101
|
var app=document.getElementById("app");if(!app)return;
|
|
4196
4102
|
var d=extractResult(result);
|
|
@@ -4202,7 +4108,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4202
4108
|
// distinctive — it names its marketplace — and because an insights payload
|
|
4203
4109
|
// carries a products array of listings, not posts, which the gallery below
|
|
4204
4110
|
// would happily draw as blank cards.
|
|
4205
|
-
if(
|
|
4111
|
+
if(d.marketplace){renderMarketplace(d);return;}
|
|
4206
4112
|
|
|
4207
4113
|
// Own account. Checked before the feeds below because all three shapes are
|
|
4208
4114
|
// distinctive and none of them carries posts, threads or an analysis.
|
|
@@ -4652,12 +4558,6 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
4652
4558
|
+'<div style="display:flex;align-items:center;gap:7px;font-size:16px;font-weight:700;margin-bottom:10px">'+mpIcon("share",16)+"<span>Repurposed</span></div>"
|
|
4653
4559
|
+versionsHtml+"</div></div>";return;}
|
|
4654
4560
|
|
|
4655
|
-
// A marketplace scan or a single product. Matched on "marketplace", which
|
|
4656
|
-
// every one of these results carries and nothing else here does — the
|
|
4657
|
-
// check has to come before the "d.platform" one below, because a scan's
|
|
4658
|
-
// raw product records carry a platform of their own.
|
|
4659
|
-
if(d.marketplace&&(d.rollup||d.product||Array.isArray(d.products))){
|
|
4660
|
-
app.innerHTML=marketCard(d);setTimeout(reportSize,50);return;}
|
|
4661
4561
|
// Single post
|
|
4662
4562
|
if(d.post||d.platform){app.innerHTML=postCard(d.post||d,true);return;}
|
|
4663
4563
|
// Fallback. The guidance field is prose written for the model, not data —
|
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": {
|