@nooticr/mcp 1.26.27 → 1.26.29

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.
@@ -14,7 +14,7 @@ import { OUTPUT_SCHEMAS, anyObject } from "./output-schemas.js";
14
14
  import { deriveHashtags, derivedNote } from "./hashtags.js";
15
15
  import { createTaskStore, registerSlowTool } from "./tasks.js";
16
16
  import { COMMENT_CATEGORIES, COMMENT_SENTIMENTS, platformFromUrl, reviewGuidance, toEvidence, } from "./comment-review.js";
17
- import { costSentence, EVIDENCE_PLANS, fetchBillingNote, frameIndex, framesToBlocks, planCalls, scoreDraftGuidance, } from "./evidence.js";
17
+ import { costSentence, EVIDENCE_PLANS, fetchBillingNote, frameIndex, framesToBlocks, planCalls, scoreDraftGuidance, KNOWN_PLATFORMS, platformFailureGuidance, } from "./evidence.js";
18
18
  import { confirmSpend, declinedResult, searchMentionsCost, SEARCH_PLATFORMS, DISCOVERABLE_PLATFORMS, } from "./spend.js";
19
19
  import { BackendWatchStore, MemoryWatchStore, registerWatchlist, } from "./watchlist.js";
20
20
  import { registerJobTools } from "./jobs.js";
@@ -25,7 +25,7 @@ import { loadingPlansJson } from "./loading-plans.js";
25
25
  import { registerHandoff } from "./handoff.js";
26
26
  import { registerCollabTools } from "./collab.js";
27
27
  /** Current MCP server version — bumped on every deploy for traceability. */
28
- export const MCP_SERVER_VERSION = "1.26.27";
28
+ export const MCP_SERVER_VERSION = "1.26.29";
29
29
  /** MCP Apps extension identifier */
30
30
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
31
31
  /** MIME type for MCP Apps HTML resources */
@@ -342,6 +342,21 @@ function toolError(prefix, err) {
342
342
  isError: true,
343
343
  };
344
344
  }
345
+ // A network that ERRORED is not a network that came back empty, and the two
346
+ // want opposite responses: an empty result means try a different query, an
347
+ // error means try a different network or tell the user. Nothing said which,
348
+ // so a host did the one thing that cannot work — re-ran the same call with
349
+ // the words changed, three times, each a paid upstream call. The backend
350
+ // names the platform in these messages ("... failed: reddit: ..."), so the
351
+ // split is recoverable here without every one of the 26 call sites having to
352
+ // pass its arguments down.
353
+ const platform = KNOWN_PLATFORMS.find((p) => new RegExp(`(^|[^a-z])${p}([^a-z]|$)`, "i").test(msg));
354
+ if (platform) {
355
+ return {
356
+ content: [{ type: "text", text: `${prefix}. ${platformFailureGuidance({ platform, message: msg })}` }],
357
+ isError: true,
358
+ };
359
+ }
345
360
  return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
346
361
  }
347
362
  /**
@@ -377,16 +392,34 @@ async function runEvidence(tool, args, client) {
377
392
  extra = { unavailable: plan.also.via };
378
393
  }
379
394
  }
380
- // The guidance leads. A model reads the first text block in a result, and
381
- // that is where the account of what to produce has to be — everything below
382
- // it is material with no instruction attached.
395
+ // The guidance rides in BOTH channels, and the structured one is the one
396
+ // that arrives.
397
+ //
398
+ // This used to say "the guidance leads: a model reads the first text block in
399
+ // a result, and that is where the account of what to produce has to be". That
400
+ // is what the MCP spec allows, and it is not what hosts do. Claude Code
401
+ // discards every `content` text block whenever a result carries
402
+ // `structuredContent`, keeps the non-text blocks, and shows the model the
403
+ // serialised JSON instead — unconditionally, with no setting, and without
404
+ // consulting `outputSchema`. Every tool here declares one, so every guidance
405
+ // string this file writes was landing nowhere.
406
+ //
407
+ // Measured before changing it, over 59 real runs of the quest suite: 0
408
+ // guidance phrases across 175 tool results. `analyze_post` handed back frames,
409
+ // a transcript and 2,000 characters ending in "call show_analysis when you are
410
+ // done"; the model wrote the analysis and delivered it in chat, and
411
+ // show_analysis was called in 1 run of 12.
412
+ //
413
+ // The text block stays for hosts that honour it (and for the images, which
414
+ // must sit in `content`). `guidance` on the payload is what reaches the rest.
415
+ // See docs/testing/tool-chaining-quests.md.
383
416
  const billing = nothingToFetch
384
417
  ? "Nothing was fetched for this call, so nothing was charged."
385
418
  : fetchBillingNote(tool);
386
- const content = [
387
- { type: "text", text: `${plan.guidance(args)}\n\n${billing}` },
388
- ];
419
+ const guidance = `${plan.guidance(args)}\n\n${billing}`;
420
+ const content = [{ type: "text", text: guidance }];
389
421
  const out = {
422
+ guidance,
390
423
  // Kept, and constant. It is not an echo of an argument — the job tools in
391
424
  // jobs.ts set the same key with no argument to echo — it marks a payload as
392
425
  // material the caller still has to read. Dropping it would break anything
@@ -525,7 +558,12 @@ export function createMcpServer(rawMakeClient, opts) {
525
558
  // a search_mentions result render in the monitoring view.
526
559
  "answer_my_audience",
527
560
  "show_audience_replies",
528
- "track_competitor",
561
+ "track_creator",
562
+ // The comparison track_creator computes per creator and never puts side
563
+ // by side, plus the free view that draws the table once a read is written.
564
+ "compare_creators",
565
+ "watchlist_standings",
566
+ "show_standings",
529
567
  "who_should_i_work_with",
530
568
  "why_did_this_underperform",
531
569
  "what_should_i_make_next",
@@ -533,6 +571,10 @@ export function createMcpServer(rawMakeClient, opts) {
533
571
  // hits rather than comments, so it gets its own view rather than the
534
572
  // monitoring one.
535
573
  "search_spoken_mentions",
574
+ // Prospect discovery: the same keyword search as discover_social_posts, but
575
+ // widened into the shapes a complaint takes rather than the topic it is
576
+ // about. Returns `posts`, so the gallery view draws it.
577
+ "find_people_with_problem",
536
578
  // Own-account intelligence (own-account.ts). list_own_apps stays
537
579
  // view-less like watch_creator — it lists metadata, nothing to draw.
538
580
  // get_scheduled_posts and get_post_performance return a `posts` array
@@ -902,7 +944,11 @@ export function createMcpServer(rawMakeClient, opts) {
902
944
  "Work out their niche, recurring themes, hook formula, what over- and under-performs and who " +
903
945
  "their audience is, reading the spread of the numbers rather than only the best post, and " +
904
946
  `name the posts you reason from. ${costSentence("analyze_creator_profile")} ` +
905
- "Use for the teardown itself; find_hook_pattern fetches the same posts and asks only for the formula.",
947
+ "Use for the teardown itself; find_hook_pattern fetches the same posts and asks only for the " +
948
+ "formula. NOT for keeping an eye on a rival over time: \"track X\", \"how are they doing " +
949
+ "lately\", \"what have they shipped since I last looked\" are track_creator, which scores " +
950
+ "each post against that creator's own median and remembers where you left off. This tool " +
951
+ "returns the posts and no verdict, and remembers nothing between calls.",
906
952
  _meta: {
907
953
  ui: { resourceUri: uiResource("analyze_creator_profile") },
908
954
  "ui/resourceUri": uiResource("analyze_creator_profile"),
@@ -1155,9 +1201,14 @@ export function createMcpServer(rawMakeClient, opts) {
1155
1201
  const res = await client.callTool("get_post_comments", { ...args });
1156
1202
  const structured = (res.structured ?? {});
1157
1203
  const comments = toEvidence(args.url, structured.comments);
1204
+ const guidance = reviewGuidance(args.url, comments.length);
1158
1205
  return {
1159
- content: [{ type: "text", text: reviewGuidance(args.url, comments.length) }],
1206
+ content: [{ type: "text", text: guidance }],
1160
1207
  structuredContent: {
1208
+ // See runEvidence: a text block alone reaches no host that renders
1209
+ // structuredContent, and this is the tool show_comment_review's whole
1210
+ // chain hangs off.
1211
+ guidance,
1161
1212
  mode: "evidence",
1162
1213
  url: args.url,
1163
1214
  platform: structured.platform ?? null,
@@ -1418,6 +1469,62 @@ export function createMcpServer(rawMakeClient, opts) {
1418
1469
  },
1419
1470
  };
1420
1471
  });
1472
+ server.registerTool("show_standings", {
1473
+ title: "Show Standings",
1474
+ description: "Draw the standings you read out of compare_creators or watchlist_standings. Free, and makes " +
1475
+ "no requests — it only renders what you pass it: one row per creator with their window, their " +
1476
+ "own median, how often they beat it and how hard, and the post that did best. Pass `ranking` " +
1477
+ "to say which axis you ordered on, and `tooThin` for the creators whose window was too short " +
1478
+ "to rank — a table that hides that is the one mistake this view can make on your behalf. " +
1479
+ "Call this after you have decided what the numbers mean, not instead of deciding.",
1480
+ _meta: {
1481
+ ui: { resourceUri: uiResource("show_standings") },
1482
+ "ui/resourceUri": uiResource("show_standings"),
1483
+ "openai/outputTemplate": appsSdkResource("show_standings"),
1484
+ },
1485
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
1486
+ outputSchema: OUTPUT_SCHEMAS.show_standings,
1487
+ inputSchema: z
1488
+ .object({
1489
+ creators: z
1490
+ .array(anyObject())
1491
+ .min(1)
1492
+ .max(25)
1493
+ .describe("The creator rows, in the order you decided on — the same shape compare_creators returned " +
1494
+ "(handle, platform, window, baseline, hitRate, medianWinRatio, best, ...)."),
1495
+ metric: z.string().optional().describe("Which stat the standings are on (views, likes, ...)."),
1496
+ ranking: z
1497
+ .string()
1498
+ .optional()
1499
+ .describe('What you ordered on and why — "how often they land one" and "how big it is when they do" ' +
1500
+ "usually disagree, so the reader needs to know which they are looking at."),
1501
+ verdict: z.string().optional().describe("Your read, in a sentence or two."),
1502
+ tooThin: z
1503
+ .array(z.string())
1504
+ .optional()
1505
+ .describe("Handles whose window was too short to rank. Drawn as unranked rather than bottom, " +
1506
+ "because missing from a comparison is not the same as losing it."),
1507
+ })
1508
+ .strict(),
1509
+ }, async (args) => {
1510
+ return {
1511
+ content: [
1512
+ {
1513
+ type: "text",
1514
+ text: `Showing standings for ${args.creators.length} creator${args.creators.length === 1 ? "" : "s"}.` +
1515
+ (args.verdict ? ` ${args.verdict}` : ""),
1516
+ },
1517
+ ],
1518
+ structuredContent: {
1519
+ creators: args.creators,
1520
+ metric: args.metric ?? "views",
1521
+ ranking: args.ranking ?? null,
1522
+ verdict: args.verdict ?? null,
1523
+ tooThin: args.tooThin ?? [],
1524
+ standings: true,
1525
+ },
1526
+ };
1527
+ });
1421
1528
  server.registerTool("show_comparison", {
1422
1529
  title: "Show Comparison",
1423
1530
  description: "Display a comparison you wrote after compare_posts fetched the first post and you fetched " +
@@ -1860,16 +1967,24 @@ export function createMcpServer(rawMakeClient, opts) {
1860
1967
  // argument: the draft is the caller's own text, so the backend was only ever
1861
1968
  // being paid to hold it up against a standard, and the standard is what
1862
1969
  // comes back instead. Nothing here can fail, which is why nothing is caught.
1863
- async (args) => ({
1864
- content: [{ type: "text", text: scoreDraftGuidance(args.draft, String(args.platform ?? "")) }],
1865
- structuredContent: {
1866
- draft: args.draft,
1867
- platform: args.platform ?? "tiktok",
1868
- // Same shape the free tools use, so a caller totting up a session's spend
1869
- // does not have to special-case this one.
1870
- mcpCredits: { cost: 0 },
1871
- },
1872
- }));
1970
+ async (args) => {
1971
+ const guidance = scoreDraftGuidance(args.draft, String(args.platform ?? ""));
1972
+ return {
1973
+ content: [{ type: "text", text: guidance }],
1974
+ structuredContent: {
1975
+ // The rubric IS this tool's answer — it fetches nothing and scores
1976
+ // nothing itself. In a text block it reached no host that renders
1977
+ // structuredContent, which made the tool return its own input and a
1978
+ // credit note. See runEvidence.
1979
+ guidance,
1980
+ draft: args.draft,
1981
+ platform: args.platform ?? "tiktok",
1982
+ // Same shape the free tools use, so a caller totting up a session's spend
1983
+ // does not have to special-case this one.
1984
+ mcpCredits: { cost: 0 },
1985
+ },
1986
+ };
1987
+ });
1873
1988
  server.registerTool("repurpose_post", {
1874
1989
  title: "Repurpose Post",
1875
1990
  description: "The source post, its transcript and its stats, for you to rewrite for other surfaces — X " +
@@ -2186,7 +2301,7 @@ export function createMcpServer(rawMakeClient, opts) {
2186
2301
  }
2187
2302
  });
2188
2303
  registerPrompts(server);
2189
- // One store for both. track_competitor keeps its "since I last looked" marker
2304
+ // One store for both. track_creator keeps its "since I last looked" marker
2190
2305
  // on the same watchlist entries, in its own field — two stores would mean a
2191
2306
  // creator you watch and a creator you track were different people.
2192
2307
  //
@@ -2782,7 +2782,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
2782
2782
  /**
2783
2783
  * The one line a competitor sweep or an underperformance check actually
2784
2784
  * exists to produce — ratio to the creator's own median, and the verdict
2785
- * that ratio earns. track_competitor and why_did_this_underperform attach
2785
+ * that ratio earns. track_creator and why_did_this_underperform attach
2786
2786
  * "standing" to every post they score (see performance.ts), and until this
2787
2787
  * existed postCard rendered the same view/like/comment pills any gallery
2788
2788
  * gets, so the computed comparison a person paid for never reached the
@@ -3661,6 +3661,64 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
3661
3661
  if(d.posts&&Array.isArray(d.posts)){
3662
3662
  if(!d.posts.length){app.innerHTML='<div class="empty-state fade-in"><div class="icon">🔍</div><div class="text">No posts found</div></div>';return;}
3663
3663
  app.innerHTML=galleryWrap(d.posts.map(function(p){return postCard(p,false,true);}).join(""),d.posts.length)+pickBarHtml();initPicks();return;}
3664
+ // Standings: creators on one normalised axis. A table, because the question
3665
+ // is comparative — and every row carries the window it was scored over. A
3666
+ // hit rate whose denominator is invisible cannot be told apart from one
3667
+ // that means something, which is why these numbers were computed per
3668
+ // creator and never put side by side before (issue #30).
3669
+ if(Array.isArray(d.creators)&&(d.standings||d.tool==="compare_creators"||d.tool==="watchlist_standings")){
3670
+ var thin=Array.isArray(d.tooThin)?d.tooThin:[];
3671
+ var floor=typeof d.thinWindow==="number"?d.thinWindow:8;
3672
+ var above=typeof d.aboveRatio==="number"?d.aboveRatio:1.25;
3673
+ var met=esc(d.metric||"views");
3674
+ var rows=d.creators.map(function(c){
3675
+ var un=c.unavailable;
3676
+ // A window shorter than the floor, or one the model flagged, is drawn
3677
+ // unranked. Bottom of a table is a claim; unranked is the truth.
3678
+ var short=!un&&(thin.indexOf(String(c.handle))>=0||(typeof c.window==="number"&&c.window<floor));
3679
+ var med=c.baseline&&c.baseline.median!=null?fmtNum(c.baseline.median):"-";
3680
+ var hit=c.hitRate!=null?Math.round(c.hitRate*100)+"%":"-";
3681
+ var win=c.medianWinRatio!=null?(Math.round(c.medianWinRatio*100)/100)+"x":"-";
3682
+ var best=c.best&&(c.best.title||c.best.caption)?String(c.best.title||c.best.caption):"";
3683
+ return '<tr style="border-top:1px solid var(--border-soft)'+(un?";opacity:.6":"")+'">'
3684
+ +'<td style="padding:9px 10px;font-weight:600;white-space:nowrap">@'+esc(c.handle)
3685
+ +(c.platform?'<span style="font-weight:400;color:var(--muted)"> · '+esc(c.platform)+"</span>":"")
3686
+ +(short?'<span style="margin-left:6px;font-size:10px;font-weight:700;color:var(--warn)">too thin to rank</span>':"")
3687
+ +"</td>"
3688
+ +(un
3689
+ ?'<td colspan="4" style="padding:9px 10px;color:var(--muted);font-size:12.5px">'+esc(String(un))+"</td>"
3690
+ :'<td style="padding:9px 10px;text-align:right;font-variant-numeric:tabular-nums">'+fmtNum(c.window||0)+"</td>"
3691
+ +'<td style="padding:9px 10px;text-align:right;font-variant-numeric:tabular-nums">'+med+"</td>"
3692
+ +'<td style="padding:9px 10px;text-align:right;font-variant-numeric:tabular-nums">'+hit+"</td>"
3693
+ +'<td style="padding:9px 10px;text-align:right;font-variant-numeric:tabular-nums">'+win+"</td>")
3694
+ +"</tr>"
3695
+ +(best&&!un
3696
+ ?'<tr><td colspan="5" style="padding:0 10px 9px 10px;color:var(--muted);font-size:12px">best: '+esc(best.slice(0,120))+"</td></tr>"
3697
+ :"");
3698
+ }).join("");
3699
+ app.innerHTML='<div class="card card-wide fade-in"><div class="card-body">'
3700
+ +'<div style="font-size:16px;font-weight:700">Standings</div>'
3701
+ +'<div class="faint" style="font-size:11.5px;color:var(--muted);margin:2px 0 10px">'
3702
+ +d.creators.length+" creator"+(d.creators.length===1?"":"s")+" · each against their OWN median "+met
3703
+ +(d.watching!=null?" · watchlist of "+fmtNum(d.watching):"")+"</div>"
3704
+ +(d.ranking?'<div style="font-size:12.5px;margin:0 0 8px">Ordered on: '+esc(String(d.ranking))+"</div>":"")
3705
+ +'<div style="overflow-x:auto"><table style="width:100%;border-collapse:collapse;font-size:13px">'
3706
+ +'<thead><tr style="text-align:left;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.05em">'
3707
+ +'<th style="padding:0 10px 6px">Creator</th>'
3708
+ +'<th style="padding:0 10px 6px;text-align:right">Window</th>'
3709
+ +'<th style="padding:0 10px 6px;text-align:right">Their median</th>'
3710
+ +'<th style="padding:0 10px 6px;text-align:right">Beat it</th>'
3711
+ +'<th style="padding:0 10px 6px;text-align:right">By</th>'
3712
+ +"</tr></thead><tbody>"+rows+"</tbody></table></div>"
3713
+ +(d.verdict?'<div style="font-size:13.5px;line-height:1.6;margin-top:12px">'+esc(String(d.verdict))+"</div>":"")
3714
+ +'<div class="faint" style="font-size:11.5px;color:var(--muted);margin-top:10px;line-height:1.6">'
3715
+ +'"Beat it" is the share of the window at or above '+above+"x that creator's own median. "
3716
+ +"A raw view count mostly measures follower count, so nothing here compares raw numbers. "
3717
+ +"A window under "+floor+" posts is one post either way, and no ratio here says whether a "
3718
+ +"baseline is itself moving — that needs a second point in time."
3719
+ +"</div></div></div>";
3720
+ return;}
3721
+
3664
3722
  // Creators
3665
3723
  if(d.creators&&Array.isArray(d.creators)){
3666
3724
  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;}
@@ -3924,8 +3982,18 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
3924
3982
 
3925
3983
  // Single post
3926
3984
  if(d.post||d.platform){app.innerHTML=postCard(d.post||d,true);return;}
3927
- // Fallback
3928
- app.innerHTML='<div class="json-block fade-in">'+esc(JSON.stringify(d,null,2))+"</div>";
3985
+ // Fallback. The guidance field is prose written for the model, not data —
3986
+ // dumped into the JSON block it reads as a wall of escaped text, and it is
3987
+ // the longest string in most payloads. Drawn above the block and dropped
3988
+ // from it. This is also what finally gives score_draft a view worth
3989
+ // looking at: its rubric IS its answer, and the card used to show the
3990
+ // echoed draft and a credit cost with the rubric nowhere on screen.
3991
+ var rest=d,lead="";
3992
+ if(d&&typeof d==="object"&&typeof d.guidance==="string"){
3993
+ lead='<div class="fade-in" style="white-space:pre-wrap;line-height:1.5;margin-bottom:12px">'+esc(d.guidance)+"</div>";
3994
+ rest={};for(var gk in d){if(gk!=="guidance")rest[gk]=d[gk];}
3995
+ }
3996
+ app.innerHTML=lead+'<div class="json-block fade-in">'+esc(JSON.stringify(rest,null,2))+"</div>";
3929
3997
  setTimeout(reportSize,50);
3930
3998
  }
3931
3999
 
@@ -33,11 +33,18 @@
33
33
  * registers, and only one registration under a name can win — but renaming
34
34
  * either side would break whichever hosts had already learned the old one,
35
35
  * for a tool whose whole promise is "what you called before still works". So
36
- * nothing about the surface moved. `watch_creator`, `unwatch_creator`,
37
- * `catch_up_watchlist` and `track_competitor` keep their names, arguments and
36
+ * nothing about the surface moved for THAT change. `watch_creator`,
37
+ * `unwatch_creator` and `catch_up_watchlist` keep their names, arguments and
38
38
  * behaviour; only the bytes moved, and the backend's own twins stay
39
39
  * unregistered here because they would be a second, disconnected list.
40
40
  *
41
+ * `track_creator` is the one exception, and for an unrelated reason: it was
42
+ * `track_competitor`, and that name was measured to be unreachable for the
43
+ * way most people ask for it — see its registration in jobs.ts. The backend
44
+ * still registers `track_competitor` for its own dashboard copilot, which is
45
+ * the ordinary state of two repos with two release cycles rather than a
46
+ * collision: the names no longer overlap, so nothing has to win.
47
+ *
41
48
  * The stores above are still live and still matter. They are the fallback for
42
49
  * an account with no workspace or a backend too old to have the tools, and
43
50
  * they are what the one-time migration reads from. Deleting one because "the
@@ -155,9 +162,10 @@ export class FileWatchStore {
155
162
  * and on ChatGPT had two, and a server-side scheduler could read neither.
156
163
  *
157
164
  * The swap is deliberately at `WatchStore` rather than at the tool layer.
158
- * `watch_creator`, `unwatch_creator`, `catch_up_watchlist` and
159
- * `track_competitor` keep their names, their arguments and their behaviour;
160
- * only where the bytes live changes. That is the whole reason the collision
165
+ * `watch_creator`, `unwatch_creator` and `catch_up_watchlist` keep their
166
+ * names, their arguments and their behaviour; only where the bytes live
167
+ * changes. (`track_creator` was later renamed from `track_competitor` for a
168
+ * reason that has nothing to do with storage — see above.) That is the whole reason the collision
161
169
  * with the backend's identically-named tools was not worth solving by
162
170
  * renaming anything: a host that already learned `watch_creator` keeps
163
171
  * working, and it now writes somewhere the other host can see.
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.27",
4
+ "version": "1.26.29",
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": {
@@ -38,9 +38,10 @@
38
38
  "test:e2e:visual": "playwright test tests/e2e/agentic-visual.e2e.ts tests/e2e/agentic-visual-full-app.e2e.ts",
39
39
  "test:e2e-smoke": "bash scripts/run-mechanical-e2e-smoke.sh",
40
40
  "test:e2e-smoke:fixture": "NOOTICR_E2E_BACKEND=fixture bash scripts/run-mechanical-e2e-smoke.sh",
41
- "test:agentic-e2e": "bash scripts/run-agentic-evals.sh",
41
+ "test:quests": "bash scripts/run-quests.sh",
42
42
  "contract:host": "sh scripts/host-contract.sh",
43
- "verify": "npm run build && npx tsc --noEmit && (cd cloudflare && npx tsc --noEmit) && npx vitest run && npm run contract:host && npm run conformance:mcpjam && npx playwright test",
43
+ "chain:map": "bash scripts/run-chain-map.sh",
44
+ "verify": "npm run build && npx tsc --noEmit && (cd cloudflare && npx tsc --noEmit) && npx vitest run && npm run contract:host && npm run chain:map && npm run conformance:mcpjam && npx playwright test",
44
45
  "contract:manifest": "node scripts/contract-check.mjs --from=../nooticr-server/platform-capabilities.json",
45
46
  "test:mutation": "node scripts/mutation-guard.mjs",
46
47
  "test:invariants": "node scripts/invariant-guard.mjs",