@nooticr/mcp 1.26.28 → 1.26.30
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 +8 -5
- package/dist/shared/brand-watch.js +37 -9
- package/dist/shared/evidence-digest.js +269 -0
- package/dist/shared/evidence.js +67 -0
- package/dist/shared/jobs.js +199 -22
- package/dist/shared/loading-plans.js +10 -1
- package/dist/shared/output-schemas.js +32 -2
- package/dist/shared/own-account.js +12 -2
- package/dist/shared/prompts.js +1 -1
- package/dist/shared/standings.js +5 -2
- package/dist/shared/tools-def.js +9 -3
- package/dist/shared/tools.js +110 -34
- package/dist/shared/ui-template.js +13 -3
- package/dist/shared/watchlist.js +13 -5
- package/package.json +4 -3
package/dist/shared/tools.js
CHANGED
|
@@ -14,7 +14,8 @@ 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
|
+
import { evidenceDigest } from "./evidence-digest.js";
|
|
18
19
|
import { confirmSpend, declinedResult, searchMentionsCost, SEARCH_PLATFORMS, DISCOVERABLE_PLATFORMS, } from "./spend.js";
|
|
19
20
|
import { BackendWatchStore, MemoryWatchStore, registerWatchlist, } from "./watchlist.js";
|
|
20
21
|
import { registerJobTools } from "./jobs.js";
|
|
@@ -25,7 +26,7 @@ import { loadingPlansJson } from "./loading-plans.js";
|
|
|
25
26
|
import { registerHandoff } from "./handoff.js";
|
|
26
27
|
import { registerCollabTools } from "./collab.js";
|
|
27
28
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
28
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
29
|
+
export const MCP_SERVER_VERSION = "1.26.30";
|
|
29
30
|
/** MCP Apps extension identifier */
|
|
30
31
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
31
32
|
/** MIME type for MCP Apps HTML resources */
|
|
@@ -342,6 +343,21 @@ function toolError(prefix, err) {
|
|
|
342
343
|
isError: true,
|
|
343
344
|
};
|
|
344
345
|
}
|
|
346
|
+
// A network that ERRORED is not a network that came back empty, and the two
|
|
347
|
+
// want opposite responses: an empty result means try a different query, an
|
|
348
|
+
// error means try a different network or tell the user. Nothing said which,
|
|
349
|
+
// so a host did the one thing that cannot work — re-ran the same call with
|
|
350
|
+
// the words changed, three times, each a paid upstream call. The backend
|
|
351
|
+
// names the platform in these messages ("... failed: reddit: ..."), so the
|
|
352
|
+
// split is recoverable here without every one of the 26 call sites having to
|
|
353
|
+
// pass its arguments down.
|
|
354
|
+
const platform = KNOWN_PLATFORMS.find((p) => new RegExp(`(^|[^a-z])${p}([^a-z]|$)`, "i").test(msg));
|
|
355
|
+
if (platform) {
|
|
356
|
+
return {
|
|
357
|
+
content: [{ type: "text", text: `${prefix}. ${platformFailureGuidance({ platform, message: msg })}` }],
|
|
358
|
+
isError: true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
345
361
|
return { content: [{ type: "text", text: `${prefix}: ${msg}` }], isError: true };
|
|
346
362
|
}
|
|
347
363
|
/**
|
|
@@ -377,16 +393,34 @@ async function runEvidence(tool, args, client) {
|
|
|
377
393
|
extra = { unavailable: plan.also.via };
|
|
378
394
|
}
|
|
379
395
|
}
|
|
380
|
-
// The guidance
|
|
381
|
-
// that
|
|
382
|
-
//
|
|
396
|
+
// The guidance rides in BOTH channels, and the structured one is the one
|
|
397
|
+
// that arrives.
|
|
398
|
+
//
|
|
399
|
+
// This used to say "the guidance leads: a model reads the first text block in
|
|
400
|
+
// a result, and that is where the account of what to produce has to be". That
|
|
401
|
+
// is what the MCP spec allows, and it is not what hosts do. Claude Code
|
|
402
|
+
// discards every `content` text block whenever a result carries
|
|
403
|
+
// `structuredContent`, keeps the non-text blocks, and shows the model the
|
|
404
|
+
// serialised JSON instead — unconditionally, with no setting, and without
|
|
405
|
+
// consulting `outputSchema`. Every tool here declares one, so every guidance
|
|
406
|
+
// string this file writes was landing nowhere.
|
|
407
|
+
//
|
|
408
|
+
// Measured before changing it, over 59 real runs of the quest suite: 0
|
|
409
|
+
// guidance phrases across 175 tool results. `analyze_post` handed back frames,
|
|
410
|
+
// a transcript and 2,000 characters ending in "call show_analysis when you are
|
|
411
|
+
// done"; the model wrote the analysis and delivered it in chat, and
|
|
412
|
+
// show_analysis was called in 1 run of 12.
|
|
413
|
+
//
|
|
414
|
+
// The text block stays for hosts that honour it (and for the images, which
|
|
415
|
+
// must sit in `content`). `guidance` on the payload is what reaches the rest.
|
|
416
|
+
// See docs/testing/tool-chaining-quests.md.
|
|
383
417
|
const billing = nothingToFetch
|
|
384
418
|
? "Nothing was fetched for this call, so nothing was charged."
|
|
385
419
|
: fetchBillingNote(tool);
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
];
|
|
420
|
+
const guidance = `${plan.guidance(args)}\n\n${billing}`;
|
|
421
|
+
const content = [{ type: "text", text: guidance }];
|
|
389
422
|
const out = {
|
|
423
|
+
guidance,
|
|
390
424
|
// Kept, and constant. It is not an echo of an argument — the job tools in
|
|
391
425
|
// jobs.ts set the same key with no argument to echo — it marks a payload as
|
|
392
426
|
// material the caller still has to read. Dropping it would break anything
|
|
@@ -411,6 +445,19 @@ async function runEvidence(tool, args, client) {
|
|
|
411
445
|
// rather than "no fetch was made".
|
|
412
446
|
if (plan.also && !nothingToFetch)
|
|
413
447
|
out[plan.also.via] = extra;
|
|
448
|
+
// The evidence has to ride in the text block too, and it is written last
|
|
449
|
+
// because `out` is what it renders — the fetched payload, plus whatever the
|
|
450
|
+
// `also` call added, minus the frames that became image blocks.
|
|
451
|
+
//
|
|
452
|
+
// Same reason the guidance is in both channels (#44/#51), pointed the other
|
|
453
|
+
// way: Claude Code drops these text blocks and reads `structuredContent`,
|
|
454
|
+
// while a host rendering a UI view hands the model the text blocks and gives
|
|
455
|
+
// `structuredContent` to the widget. A transcript that exists only in the
|
|
456
|
+
// second one is a transcript the model was told to read and never saw (#59).
|
|
457
|
+
const digest = evidenceDigest(out);
|
|
458
|
+
if (digest) {
|
|
459
|
+
content[0] = { type: "text", text: `${guidance}\n\n---\n\n${digest}` };
|
|
460
|
+
}
|
|
414
461
|
return { content, structuredContent: out };
|
|
415
462
|
}
|
|
416
463
|
export function createMcpServer(rawMakeClient, opts) {
|
|
@@ -525,8 +572,8 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
525
572
|
// a search_mentions result render in the monitoring view.
|
|
526
573
|
"answer_my_audience",
|
|
527
574
|
"show_audience_replies",
|
|
528
|
-
"
|
|
529
|
-
// The comparison
|
|
575
|
+
"track_creator",
|
|
576
|
+
// The comparison track_creator computes per creator and never puts side
|
|
530
577
|
// by side, plus the free view that draws the table once a read is written.
|
|
531
578
|
"compare_creators",
|
|
532
579
|
"watchlist_standings",
|
|
@@ -538,6 +585,10 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
538
585
|
// hits rather than comments, so it gets its own view rather than the
|
|
539
586
|
// monitoring one.
|
|
540
587
|
"search_spoken_mentions",
|
|
588
|
+
// Prospect discovery: the same keyword search as discover_social_posts, but
|
|
589
|
+
// widened into the shapes a complaint takes rather than the topic it is
|
|
590
|
+
// about. Returns `posts`, so the gallery view draws it.
|
|
591
|
+
"find_people_with_problem",
|
|
541
592
|
// Own-account intelligence (own-account.ts). list_own_apps stays
|
|
542
593
|
// view-less like watch_creator — it lists metadata, nothing to draw.
|
|
543
594
|
// get_scheduled_posts and get_post_performance return a `posts` array
|
|
@@ -907,7 +958,11 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
907
958
|
"Work out their niche, recurring themes, hook formula, what over- and under-performs and who " +
|
|
908
959
|
"their audience is, reading the spread of the numbers rather than only the best post, and " +
|
|
909
960
|
`name the posts you reason from. ${costSentence("analyze_creator_profile")} ` +
|
|
910
|
-
"Use for the teardown itself; find_hook_pattern fetches the same posts and asks only for the
|
|
961
|
+
"Use for the teardown itself; find_hook_pattern fetches the same posts and asks only for the " +
|
|
962
|
+
"formula. NOT for keeping an eye on a rival over time: \"track X\", \"how are they doing " +
|
|
963
|
+
"lately\", \"what have they shipped since I last looked\" are track_creator, which scores " +
|
|
964
|
+
"each post against that creator's own median and remembers where you left off. This tool " +
|
|
965
|
+
"returns the posts and no verdict, and remembers nothing between calls.",
|
|
911
966
|
_meta: {
|
|
912
967
|
ui: { resourceUri: uiResource("analyze_creator_profile") },
|
|
913
968
|
"ui/resourceUri": uiResource("analyze_creator_profile"),
|
|
@@ -1160,19 +1215,32 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1160
1215
|
const res = await client.callTool("get_post_comments", { ...args });
|
|
1161
1216
|
const structured = (res.structured ?? {});
|
|
1162
1217
|
const comments = toEvidence(args.url, structured.comments);
|
|
1218
|
+
const guidance = reviewGuidance(args.url, comments.length);
|
|
1219
|
+
// "Here are 4 comments ... Read them and classify each one" was a false
|
|
1220
|
+
// sentence in the channel it was written in: the comments were only ever
|
|
1221
|
+
// in `structuredContent`, which a host rendering the view keeps for the
|
|
1222
|
+
// widget. The model was asked to classify comments it could not see, and
|
|
1223
|
+
// to return the `id` of each — so the digest renders ids too (#59).
|
|
1224
|
+
const payload = {
|
|
1225
|
+
// See runEvidence: a text block alone reaches no host that renders
|
|
1226
|
+
// structuredContent, and this is the tool show_comment_review's whole
|
|
1227
|
+
// chain hangs off.
|
|
1228
|
+
guidance,
|
|
1229
|
+
mode: "evidence",
|
|
1230
|
+
url: args.url,
|
|
1231
|
+
platform: structured.platform ?? null,
|
|
1232
|
+
commentCount: comments.length,
|
|
1233
|
+
comments,
|
|
1234
|
+
// Passed through rather than re-derived: the platform's own clustering
|
|
1235
|
+
// is evidence too, and it is already paid for.
|
|
1236
|
+
themes: structured.themes ?? [],
|
|
1237
|
+
mcpCredits: structured.mcpCredits ?? null,
|
|
1238
|
+
};
|
|
1163
1239
|
return {
|
|
1164
|
-
content: [
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
platform: structured.platform ?? null,
|
|
1169
|
-
commentCount: comments.length,
|
|
1170
|
-
comments,
|
|
1171
|
-
// Passed through rather than re-derived: the platform's own clustering
|
|
1172
|
-
// is evidence too, and it is already paid for.
|
|
1173
|
-
themes: structured.themes ?? [],
|
|
1174
|
-
mcpCredits: structured.mcpCredits ?? null,
|
|
1175
|
-
},
|
|
1240
|
+
content: [
|
|
1241
|
+
{ type: "text", text: `${guidance}\n\n---\n\n${evidenceDigest(payload)}` },
|
|
1242
|
+
],
|
|
1243
|
+
structuredContent: payload,
|
|
1176
1244
|
};
|
|
1177
1245
|
}
|
|
1178
1246
|
catch (err) {
|
|
@@ -1921,16 +1989,24 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1921
1989
|
// argument: the draft is the caller's own text, so the backend was only ever
|
|
1922
1990
|
// being paid to hold it up against a standard, and the standard is what
|
|
1923
1991
|
// comes back instead. Nothing here can fail, which is why nothing is caught.
|
|
1924
|
-
async (args) =>
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1992
|
+
async (args) => {
|
|
1993
|
+
const guidance = scoreDraftGuidance(args.draft, String(args.platform ?? ""));
|
|
1994
|
+
return {
|
|
1995
|
+
content: [{ type: "text", text: guidance }],
|
|
1996
|
+
structuredContent: {
|
|
1997
|
+
// The rubric IS this tool's answer — it fetches nothing and scores
|
|
1998
|
+
// nothing itself. In a text block it reached no host that renders
|
|
1999
|
+
// structuredContent, which made the tool return its own input and a
|
|
2000
|
+
// credit note. See runEvidence.
|
|
2001
|
+
guidance,
|
|
2002
|
+
draft: args.draft,
|
|
2003
|
+
platform: args.platform ?? "tiktok",
|
|
2004
|
+
// Same shape the free tools use, so a caller totting up a session's spend
|
|
2005
|
+
// does not have to special-case this one.
|
|
2006
|
+
mcpCredits: { cost: 0 },
|
|
2007
|
+
},
|
|
2008
|
+
};
|
|
2009
|
+
});
|
|
1934
2010
|
server.registerTool("repurpose_post", {
|
|
1935
2011
|
title: "Repurpose Post",
|
|
1936
2012
|
description: "The source post, its transcript and its stats, for you to rewrite for other surfaces — X " +
|
|
@@ -2247,7 +2323,7 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
2247
2323
|
}
|
|
2248
2324
|
});
|
|
2249
2325
|
registerPrompts(server);
|
|
2250
|
-
// One store for both.
|
|
2326
|
+
// One store for both. track_creator keeps its "since I last looked" marker
|
|
2251
2327
|
// on the same watchlist entries, in its own field — two stores would mean a
|
|
2252
2328
|
// creator you watch and a creator you track were different people.
|
|
2253
2329
|
//
|
|
@@ -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.
|
|
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
|
|
@@ -3982,8 +3982,18 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3982
3982
|
|
|
3983
3983
|
// Single post
|
|
3984
3984
|
if(d.post||d.platform){app.innerHTML=postCard(d.post||d,true);return;}
|
|
3985
|
-
// Fallback
|
|
3986
|
-
|
|
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>";
|
|
3987
3997
|
setTimeout(reportSize,50);
|
|
3988
3998
|
}
|
|
3989
3999
|
|
package/dist/shared/watchlist.js
CHANGED
|
@@ -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`,
|
|
37
|
-
* `
|
|
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
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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.
|
|
4
|
+
"version": "1.26.30",
|
|
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:
|
|
41
|
+
"test:quests": "bash scripts/run-quests.sh",
|
|
42
42
|
"contract:host": "sh scripts/host-contract.sh",
|
|
43
|
-
"
|
|
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",
|