@nooticr/mcp 1.26.41 → 1.26.42

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.
@@ -39,9 +39,15 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
39
39
  get_post_comments:"Get Post Comments",
40
40
  search_creators:"Search Creators",
41
41
  get_similar_creators:"Similar Creators",
42
+ suggest_creator_identity:"Same Person Elsewhere",
42
43
  discover_sounds:"Discover Sounds",
43
44
  understand_social_post:"Understand Social Post",
44
45
  check_nooticr_credits:"Check Credits",
46
+ list_tool_runs:"Tool Runs",
47
+ get_tool_run:"Tool Run",
48
+ list_watchlist:"Watchlist",
49
+ detect_spoken_mentions:"Spoken Mentions",
50
+ nooticr_getting_started:"Getting Started",
45
51
  compose_sequence:"Compose Sequence",
46
52
  overlay_bake:"Overlay Bake",
47
53
  spawn_variants:"Spawn Variants",
@@ -68,9 +74,15 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
68
74
  get_post_comments:"Top comments on a post — sentiment, themes, viral threads.",
69
75
  search_creators:"Find creators in a niche by engagement, followers, and content.",
70
76
  get_similar_creators:"Find creators similar to a given handle.",
77
+ suggest_creator_identity:"Accounts on other networks that may be the same person, with the evidence.",
71
78
  discover_sounds:"Trending sounds and music on TikTok/Instagram.",
72
79
  understand_social_post:"The same frames and transcript, for a description of what happens on screen.",
73
80
  check_nooticr_credits:"View your Nooticr credit balance and usage.",
81
+ list_tool_runs:"Where your credits went: every call, what it cost, and whether it worked.",
82
+ get_tool_run:"One call from your history, with its full error.",
83
+ list_watchlist:"The creators you are watching, and when you last caught up on each.",
84
+ detect_spoken_mentions:"Brands named out loud in a video, beside what its caption says.",
85
+ nooticr_getting_started:"Where your account stands and what to try first.",
74
86
  compose_sequence:"AI-powered content composition for social posts.",
75
87
  overlay_bake:"Bake text/image overlays onto video or image.",
76
88
  spawn_variants:"Generate multiple content variants from a single seed.",
@@ -2997,6 +3009,85 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
2997
3009
  +"</div>";
2998
3010
  }
2999
3011
 
3012
+ // ─── Identity suggestions (suggest_creator_identity) ───
3013
+ //
3014
+ // Its own view rather than creatorCard: a candidate carries a points score
3015
+ // (50 for a shared bio link, 25 for a cross-link) that the vetting strip
3016
+ // would draw as "/100", and no follower total, because adding two accounts'
3017
+ // followers together on this evidence is exactly what the tool forbids.
3018
+ // Every card says "suggestion" on its face for the same reason.
3019
+ function renderIdentity(d){
3020
+ var seed=d.seed||{},list=d.suggestions||[];
3021
+ var searched=(d.searched||[]).join(", ");
3022
+ var head='<div style="margin-bottom:12px"><div style="font-size:15px;font-weight:700">Possibly the same person as @'
3023
+ +esc(seed.handle||"")+" on "+esc(seed.platform||"")+"</div>"
3024
+ +'<div style="font-size:12px;color:var(--muted);margin-top:3px">'
3025
+ +list.length+" suggestion"+(list.length===1?"":"s")+(searched?" · looked on "+esc(searched):"")
3026
+ +" · nothing merged, a human confirms</div>"
3027
+ +(d.note?'<div style="font-size:12px;color:var(--muted);margin-top:6px">'+esc(d.note)+"</div>":"")
3028
+ +"</div>";
3029
+ var cards=list.map(function(c){
3030
+ var high=c.confidence==="high",color=pColor(c.platform||"");
3031
+ var ev=(c.evidence||[]).map(function(e){
3032
+ var strong=e.strength==="strong";
3033
+ return '<li style="margin:3px 0"><span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:'
3034
+ +(strong?"var(--green)":"var(--muted)")+'">'+esc(e.strength||"")+"</span> "
3035
+ +'<span style="font-weight:600">'+esc(String(e.signal||"").split("_").join(" "))+"</span>"
3036
+ +(e.detail?' <span style="color:var(--muted)">· '+esc(e.detail)+"</span>":"")+"</li>";
3037
+ }).join("");
3038
+ var link=c.profileUrl&&String(c.profileUrl).indexOf("http")===0
3039
+ ?'<a href="'+esc(c.profileUrl)+'" style="font-size:12px;color:var(--fg)">Open profile</a>':"";
3040
+ return '<div class="creator-card" data-identity-candidate>'
3041
+ +'<div style="display:flex;align-items:center;gap:10px;margin-bottom:8px">'
3042
+ +'<div class="avatar-placeholder" style="flex-shrink:0;background:'+color+'20">'+pSvg(c.platform||"",22)+"</div>"
3043
+ +'<div style="min-width:0"><div style="font-size:14px;font-weight:700">'+esc(c.nickname||c.username||"")+"</div>"
3044
+ +'<div class="handle" style="overflow-wrap:anywhere">@'+esc(c.username||"")+" · "+esc(c.platform||"")+"</div></div></div>"
3045
+ +'<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;margin-bottom:6px;color:'
3046
+ +(high?"var(--green)":"var(--muted)")+'">'+(high?"likely":"possible")+" · suggestion</div>"
3047
+ +(c.followers!=null?'<div style="font-size:12px;color:var(--muted);margin-bottom:6px">'+fmtNum(c.followers)+" followers on this account alone</div>":"")
3048
+ +(ev?'<ul style="list-style:none;padding:0;margin:0;font-size:12px;line-height:1.4">'+ev+"</ul>":"")
3049
+ +(link?'<div style="margin-top:8px">'+link+"</div>":"")
3050
+ +"</div>";
3051
+ }).join("");
3052
+ var none=list.length?"":'<div class="empty-state"><div class="icon">👤</div><div class="text">No account cleared the bar. A creator with no links in their bio is hard to match this way; that is not proof they have no other accounts.</div></div>';
3053
+ var gaps=(d.unavailable||[]).map(function(u){return esc(u.platform||"")+" could not be searched";}).join(" · ");
3054
+ return '<div class="fade-in">'+head+none+(cards?'<div class="gallery">'+cards+"</div>":"")
3055
+ +(gaps?'<div style="font-size:12px;color:var(--muted);margin-top:10px">'+gaps+"</div>":"")+"</div>";
3056
+ }
3057
+
3058
+ // ─── Getting started (nooticr_getting_started) ───
3059
+ //
3060
+ // Checked before the credits card, which it would otherwise fall into: it
3061
+ // carries a balance too. A null is drawn as "could not read", never as 0 —
3062
+ // "no balance" and "no connections" are claims this view has not checked.
3063
+ function renderGettingStarted(d){
3064
+ function stat(label,val,unknown){
3065
+ return '<div style="flex:1;min-width:120px;padding:10px 12px;border:1px solid var(--border);border-radius:10px">'
3066
+ +'<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">'+esc(label)+"</div>"
3067
+ +'<div style="font-size:16px;font-weight:700;margin-top:3px">'+(val==null?'<span style="color:var(--muted);font-weight:500;font-size:13px">'+esc(unknown)+"</span>":esc(String(val)))+"</div></div>";
3068
+ }
3069
+ var head='<div style="font-size:15px;font-weight:700;margin-bottom:10px">'
3070
+ +(d.signedIn?"Where your nooticr account stands":"Not signed in to nooticr yet")+"</div>";
3071
+ var stats=d.signedIn
3072
+ ?'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:14px">'
3073
+ +stat("Credits",d.balance,"could not read")
3074
+ +stat("Connected accounts",d.connectedCount,"could not read")
3075
+ +stat("Watching",d.watching,"could not read")+"</div>"
3076
+ :"";
3077
+ var steps=(d.nextSteps||[]).map(function(n){
3078
+ var price=Number(n.credits)>0?esc(String(n.credits))+" cr":"free";
3079
+ var ex=n.example&&typeof n.example==="object"?JSON.stringify(n.example):"";
3080
+ return '<div data-next-step style="padding:10px 0;border-top:1px solid var(--border)">'
3081
+ +'<div style="display:flex;align-items:baseline;gap:8px"><code style="font-weight:700">'+esc(n.tool||"")+"</code>"
3082
+ +'<span style="margin-left:auto;font-size:11px;font-weight:700;color:'+(price==="free"?"var(--green)":"var(--muted)")+'">'+price+"</span></div>"
3083
+ +'<div style="font-size:13px;margin-top:3px">'+esc(n.why||"")+"</div>"
3084
+ +(ex?'<div style="font-size:12px;color:var(--muted);margin-top:3px;font-family:ui-monospace,monospace">'+esc(ex)+"</div>":"")
3085
+ +"</div>";
3086
+ }).join("");
3087
+ return '<div class="fade-in">'+head+stats
3088
+ +'<div class="sec-label">Try next</div>'+steps+"</div>";
3089
+ }
3090
+
3000
3091
  // ─── Vetting strip ───
3001
3092
  //
3002
3093
  // Only show_collab_shortlist sends these fields, so every other creator card
@@ -3414,8 +3505,8 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
3414
3505
  +'<div class="sec-label">Read-only — nooticr can sweep these, no account to link</div>'
3415
3506
  +'<div class="chiprow">'+readOnlyTags+"</div>"
3416
3507
  +'<div class="section-text" style="margin-top:12px;color:var(--muted)">So "monitor my '
3417
- +'brand on Weibo" works and "reply to that Weibo comment" cannot — no nooticr '
3418
- +"connection carries comment-write permission on any network.</div>"
3508
+ +'brand on Weibo" works and "reply to that Weibo comment" cannot — nooticr does not '
3509
+ +"post replies on any network, whatever a connection is allowed to do.</div>"
3419
3510
  +"</div></div>"
3420
3511
  :"")
3421
3512
  +"</div>";
@@ -4061,10 +4152,23 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4061
4152
  var drivers=(ins.drivers||[]).map(function(f){return amazonFinding(f,"driver");}).join("");
4062
4153
  var barriers=(ins.barriers||[]).map(function(f){return amazonFinding(f,"barrier");}).join("");
4063
4154
  var gaps=(ins.gaps||[]).map(function(f){return amazonFinding(f,"gap");}).join("");
4155
+ // #107: whether the listings on this card are the scan's own, re-read by
4156
+ // the server, or whatever the model re-sent. Only the first earns the
4157
+ // "check any claim" line at the bottom.
4158
+ var ver=ins.verification||null,verified=!!(ver&&ver.status==="verified");
4159
+ var unknown=(ver&&ver.unknownAsins)||[];
4064
4160
  var strengths=(ins.strengths||[]).map(function(s){
4161
+ var off=s.asin&&unknown.indexOf(s.asin)!==-1;
4065
4162
  return '<div class="amz-strength"><span class="amz-strength-brand">'+esc(s.brand||"")+"</span>"
4066
- +'<span class="amz-strength-detail">'+esc(s.detail||"")+"</span></div>";
4163
+ +'<span class="amz-strength-detail">'+esc(s.detail||"")+"</span>"
4164
+ +(off?'<span data-not-in-scan style="margin-left:6px;font-size:10px;font-weight:700;text-transform:uppercase;color:var(--red,#b42318)">not in the scan</span>':"")
4165
+ +"</div>";
4067
4166
  }).join("");
4167
+ var verBox=ver
4168
+ ?'<div data-verification="'+(verified?"verified":"unverified")+'" style="font-size:12px;padding:8px 10px;margin-bottom:10px;border-radius:8px;border:1px solid var(--border);'
4169
+ +(verified?"":"background:rgba(180,35,24,.06);")+'">'
4170
+ +'<b>'+(verified?"Listings re-read from the scan":"Listings not checked against a scan")+"</b> · "+esc(ver.note||"")+"</div>"
4171
+ :"";
4068
4172
  var angles=(ins.positioning||[]).map(function(a){
4069
4173
  return '<div class="amz-angle"><div class="amz-angle-claim">'+esc(a.angle||"")+"</div>"
4070
4174
  +(a.who?'<div class="amz-angle-row"><b>For</b> '+esc(a.who)+"</div>":"")
@@ -4073,6 +4177,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4073
4177
  +"</div>";
4074
4178
  }).join("");
4075
4179
  return '<div class="fade-in">'
4180
+ +verBox
4076
4181
  +(ins.summary?'<div class="lede-box">'+esc(ins.summary)+"</div>":"")
4077
4182
  +block("Purchase drivers","What makes someone buy. Click an id to read the review it rests on.",drivers)
4078
4183
  +block("Purchase barriers","What stops them, or brings it back.",barriers)
@@ -4080,7 +4185,8 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4080
4185
  +block("Gaps in the category","Asked for repeatedly, served by nobody in this set.",gaps)
4081
4186
  +block("Positioning angles","",angles)
4082
4187
  +'<div class="ai-note">This read is your model’s, from the reviews on this screen — not a nooticr '
4083
- +"rating of anyone’s product. Every claim above can be checked against the text it came from.</div>"
4188
+ +"rating of anyone’s product."
4189
+ +(verified||!ver?" Every claim above can be checked against the text it came from.":" The listings were not checked against a scan, so check them before relying on them.")+"</div>"
4084
4190
  +"</div>";
4085
4191
  }
4086
4192
 
@@ -4409,6 +4515,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4409
4515
  return;}
4410
4516
 
4411
4517
  // Creators
4518
+ if(Array.isArray(d.suggestions)&&d.seed){app.innerHTML=renderIdentity(d);setTimeout(reportSize,50);return;}
4412
4519
  if(d.creators&&Array.isArray(d.creators)){
4413
4520
  if(!d.creators.length){app.innerHTML='<div class="empty-state fade-in"><div class="icon">👤</div><div class="text">No creators found</div></div>';return;}
4414
4521
  app.innerHTML=galleryWrap(d.creators.map(function(c){return creatorCard(c);}).join(""),d.creators.length);return;}
@@ -4423,7 +4530,12 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4423
4530
  +'<div style="display:flex;align-items:center;gap:7px;font-size:16px;font-weight:700;margin-bottom:6px">'+mpIcon("note",16)+"<span>Transcript</span></div>"
4424
4531
  +'<div class="muted" style="font-size:13px;color:var(--muted)">'+esc(d.reason||"No transcript available.")+"</div></div></div>";
4425
4532
  return;}
4426
- var wc=d.wordCount||(d.transcript?String(d.transcript).trim().split(/\s+/).filter(Boolean).length:0);
4533
+ // No regex escape here: this file is one template literal (and the
4534
+ // page is backslash-free by test), so the old whitespace class reached
4535
+ // the browser as the letter s ("I switched to Acme grinders" counted 3
4536
+ // words). Whitespace is spelled out by char code instead.
4537
+ var WS=new RegExp("["+String.fromCharCode(32,9,10,13)+"]+");
4538
+ var wc=d.wordCount||(d.transcript?String(d.transcript).trim().split(WS).filter(Boolean).length:0);
4427
4539
  var meta=[wc+" words"];
4428
4540
  if(d.language)meta.push(esc(d.language));
4429
4541
  if(d.autoGenerated)meta.push("auto-generated");
@@ -4613,7 +4725,49 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4613
4725
  +(s.videoCount?'<div style="font-size:11px;color:var(--muted);margin-top:2px">'+fmtNum(s.videoCount)+" videos</div>":"")
4614
4726
  +"</div></div>"+player+"</div>";
4615
4727
  }).join(""),d.sounds.length);return;}
4728
+ // Run history (list_tool_runs / get_tool_run): where the credits went.
4729
+ // One row per call, the credits it actually took on the right, so the
4730
+ // expensive calls are the ones the eye lands on.
4731
+ if(Array.isArray(d.runs)||(d.run&&d.run.tool)){
4732
+ var runs=Array.isArray(d.runs)?d.runs:[d.run];
4733
+ if(!runs.length){app.innerHTML='<div class="empty-state fade-in"><div class="icon">🧾</div><div class="text">No runs in this window</div></div>';return;}
4734
+ var runRows=runs.map(function(r){
4735
+ var when=r.startedAt?String(r.startedAt).slice(0,16).split("T").join(" ")+" UTC":"";
4736
+ var secs=Number(r.durationMs)>0?(Number(r.durationMs)/1000).toFixed(1)+"s":"";
4737
+ var meta=[when,secs,r.userId?"user "+String(r.userId).slice(0,8):""].filter(Boolean).join(" • ");
4738
+ var cr=Number(r.credits)||0;
4739
+ return '<div style="display:flex;align-items:flex-start;gap:10px;padding:9px 0;border-bottom:1px solid var(--border)">'
4740
+ +'<div style="flex:1;min-width:0">'
4741
+ +'<div style="font-size:13.5px;font-weight:600">'+(r.ok===false?"⚠️ ":"")+esc(r.tool||"")+"</div>"
4742
+ +'<div style="font-size:11.5px;color:var(--muted)">'+esc(meta)+"</div>"
4743
+ +(r.error?'<div style="font-size:12px;color:var(--muted);margin-top:3px;word-break:break-word">'+esc(String(r.error))+"</div>":"")
4744
+ +"</div>"
4745
+ +'<div style="font-size:14px;font-weight:700;white-space:nowrap">'+(cr?cr+" cr":"free")+"</div></div>";
4746
+ }).join("");
4747
+ var total=d.creditsOnThisPage!=null?'<div class="section-label" style="margin-bottom:6px">'+esc(String(d.creditsOnThisPage))+" credits across "+runs.length+" call"+(runs.length===1?"":"s")+(d.scope==="workspace"?" in the workspace":"")+"</div>":"";
4748
+ app.innerHTML='<div class="card card-wide fade-in"><div class="card-body">'
4749
+ +'<div style="font-size:16px;font-weight:700;margin-bottom:8px">🧾 '+(d.run?"Tool run":"Where your credits went")+"</div>"
4750
+ +total+runRows
4751
+ +(d.nextBefore!=null?'<div style="font-size:11.5px;color:var(--muted);margin-top:8px">More runs are older than these.</div>':"")
4752
+ +"</div></div>";return;}
4753
+ // Watchlist (list_watchlist): who is being watched, and since when.
4754
+ if(Array.isArray(d.entries)&&d.watching!=null&&!d.posts&&!d.results){
4755
+ if(!d.entries.length){app.innerHTML='<div class="empty-state fade-in"><div class="icon">👀</div><div class="text">You are not watching anyone yet</div></div>';return;}
4756
+ var wlRows=d.entries.map(function(e){
4757
+ if(!e)return"";
4758
+ var since=e.lastCaughtUpAt?"caught up "+String(e.lastCaughtUpAt).slice(0,10):"never caught up";
4759
+ var added=e.addedAt?"added "+String(e.addedAt).slice(0,10):"";
4760
+ return '<div style="padding:9px 0;border-bottom:1px solid var(--border)">'
4761
+ +'<div style="font-size:13.5px;font-weight:600">@'+esc(e.handle||"")+' <span style="font-weight:400;color:var(--muted)">'+esc(e.platform||"")+"</span></div>"
4762
+ +'<div style="font-size:11.5px;color:var(--muted)">'+esc([added,since].filter(Boolean).join(" • "))+"</div>"
4763
+ +(e.note?'<div style="font-size:12px;margin-top:3px">'+esc(String(e.note))+"</div>":"")
4764
+ +"</div>";
4765
+ }).join("");
4766
+ app.innerHTML='<div class="card card-wide fade-in"><div class="card-body">'
4767
+ +'<div style="font-size:16px;font-weight:700;margin-bottom:8px">👀 Watching '+esc(String(d.watching))+"</div>"
4768
+ +wlRows+"</div></div>";return;}
4616
4769
  // Credits
4770
+ if(d.gettingStarted===true){app.innerHTML=renderGettingStarted(d);setTimeout(reportSize,50);return;}
4617
4771
  if(d.balance!=null||d.tier){
4618
4772
  var bal=Number(d.balance)||0,tier=d.tier||"",ff=d.firstFreeTools||[];
4619
4773
  var freeHtml="";
@@ -513,6 +513,45 @@ export function registerWatchlist(server, makeClient, store) {
513
513
  const entries = await store.list(owner);
514
514
  return text({ removed, id, watching: entries.length, entries });
515
515
  });
516
+ // Reading the list back (#101). The resource above says the same thing, but
517
+ // most hosts never surface a resource to the model and ChatGPT does not read
518
+ // one at all, so "who am I watching?" had no free answer: the only tools
519
+ // that returned the list were the paid catch-up and standings runs. Built on
520
+ // `store`, not the backend's own twin, for the reason the header gives.
521
+ server.registerTool("list_watchlist", {
522
+ title: "List Watchlist",
523
+ _meta: {
524
+ ui: { resourceUri: "ui://nooticr/list_watchlist" },
525
+ "ui/resourceUri": "ui://nooticr/list_watchlist",
526
+ "openai/outputTemplate": "ui://nooticr/list_watchlist.html",
527
+ },
528
+ description: "The creators on your watchlist: handle, platform, your note, when you added them and when " +
529
+ "you last caught up on them. Reads the stored list only — nothing is fetched, so there is no " +
530
+ "cost. Use it to answer 'who am I watching?', or to find the exact handle and platform " +
531
+ "unwatch_creator needs, before paying for catch_up_watchlist. No cost to call.",
532
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
533
+ inputSchema: z.object({}).strict(),
534
+ outputSchema: z
535
+ .object({
536
+ watching: orNull(z.number()),
537
+ entries: z.array(z.union([z.object({}).passthrough(), z.null()])).nullish(),
538
+ })
539
+ .passthrough(),
540
+ }, async (_args, extra) => {
541
+ const client = await makeClient({ ...extra, arguments: {} });
542
+ const entries = (await store.list(await ownerOf(client))).map((e) => ({
543
+ id: e.id,
544
+ platform: e.platform,
545
+ handle: e.handle,
546
+ note: e.note ?? null,
547
+ addedAt: e.addedAt,
548
+ // When "since I last looked" starts from; null until the first catch-up.
549
+ // The baseline's post ids stay out: they are the store's bookkeeping,
550
+ // and a long list of them is context spent on nothing.
551
+ lastCaughtUpAt: e.baseline?.capturedAt ?? null,
552
+ }));
553
+ return text({ watching: entries.length, entries });
554
+ });
516
555
  server.registerTool("catch_up_watchlist", {
517
556
  title: "Catch Up On Watchlist",
518
557
  _meta: {
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.41",
4
+ "version": "1.26.42",
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": {