@nooticr/mcp 1.26.19 → 1.26.20
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/evidence.js +62 -0
- package/dist/shared/jobs.js +38 -7
- package/dist/shared/tools.js +1 -1
- package/dist/shared/watchlist.js +3 -2
- package/package.json +1 -1
package/dist/shared/evidence.js
CHANGED
|
@@ -36,6 +36,68 @@
|
|
|
36
36
|
*/
|
|
37
37
|
import { BACKEND_CALL_CREDITS, costOf } from "./spend.js";
|
|
38
38
|
const url = (a) => String(a.url ?? "");
|
|
39
|
+
/**
|
|
40
|
+
* Platforms whose accounts can be found by NAME. This is the backend's
|
|
41
|
+
* search_creators enum, not a preference — no other network has a keyword
|
|
42
|
+
* search behind it, so on the rest a handle has to arrive from outside.
|
|
43
|
+
*/
|
|
44
|
+
export const NAME_SEARCHABLE = ["tiktok", "instagram", "xiaohongshu"];
|
|
45
|
+
/**
|
|
46
|
+
* The `platform` argument description every tool sharing the silent tiktok
|
|
47
|
+
* default should use.
|
|
48
|
+
*
|
|
49
|
+
* Saying "default tiktok" was technically true and operationally useless: it
|
|
50
|
+
* reads as a harmless convenience rather than as the thing that will answer a
|
|
51
|
+
* question about X with TikTok data. The default cannot simply be removed —
|
|
52
|
+
* hosts already call these without it — so the description has to carry the
|
|
53
|
+
* warning instead.
|
|
54
|
+
*/
|
|
55
|
+
export const PLATFORM_ARG = "Platform. Defaults to tiktok, so SET IT whenever the user named a network — otherwise a " +
|
|
56
|
+
"question about X, LinkedIn or Reddit is silently answered with TikTok data, and an empty " +
|
|
57
|
+
"result looks like the account does not exist. Accepts what get_user_posts does: tiktok, " +
|
|
58
|
+
"instagram, youtube, douyin, xiaohongshu, twitter, bilibili, linkedin, reddit, weibo.";
|
|
59
|
+
/**
|
|
60
|
+
* What to say when a handle lookup comes back empty.
|
|
61
|
+
*
|
|
62
|
+
* Written because of a real session: someone asked what a competitor was doing
|
|
63
|
+
* on X, the tool searched TikTok (the silent default), found nothing, offered
|
|
64
|
+
* to guess another TikTok handle, and gave up. Every part of that was the
|
|
65
|
+
* surface's fault. It never said which network it had searched, so the wrong
|
|
66
|
+
* one looked like an absent account; and an empty result carried no next step,
|
|
67
|
+
* so the only move left was to apologise.
|
|
68
|
+
*
|
|
69
|
+
* The fix is to be specific about what happened and to hand back the one step
|
|
70
|
+
* that actually works. We cannot search X, LinkedIn or Reddit by name — but the
|
|
71
|
+
* model calling us can search the open web, and `get_user_posts` will happily
|
|
72
|
+
* fetch any of them once it has the handle. So ask for that rather than
|
|
73
|
+
* substituting a network the user did not ask about.
|
|
74
|
+
*/
|
|
75
|
+
export function handleMissGuidance(a) {
|
|
76
|
+
const searchable = NAME_SEARCHABLE.includes(a.platform);
|
|
77
|
+
const lines = [
|
|
78
|
+
`No posts for @${a.handle} on ${a.platform}.`,
|
|
79
|
+
"",
|
|
80
|
+
a.defaulted
|
|
81
|
+
? `Note that ${a.platform} was the default here — it was not asked for. If the user named a ` +
|
|
82
|
+
"different network, call this again with `platform` set to it before concluding anything: " +
|
|
83
|
+
"an empty result on the wrong network says nothing about the account they meant."
|
|
84
|
+
: `That is the network that was searched, so the account either does not exist there, is ` +
|
|
85
|
+
"private, or uses a different handle.",
|
|
86
|
+
"",
|
|
87
|
+
searchable
|
|
88
|
+
? `${a.platform} can be searched by name: call search_creators with the brand or person as ` +
|
|
89
|
+
"the keyword to find the right handle, then come back."
|
|
90
|
+
: `${a.platform} cannot be searched by name here — there is no keyword index behind it, so ` +
|
|
91
|
+
"guessing handles will not converge. Find the account yourself with a web search, or ask " +
|
|
92
|
+
"the user for the handle or profile URL, then call this again with that. Do NOT quietly " +
|
|
93
|
+
"switch to a network you can search instead: answering about TikTok when the question " +
|
|
94
|
+
"was about X is worse than saying you need the handle.",
|
|
95
|
+
"",
|
|
96
|
+
"Say plainly that nothing was found and which network was checked. Do not present an empty " +
|
|
97
|
+
"result as evidence the competitor is inactive.",
|
|
98
|
+
];
|
|
99
|
+
return lines.filter((l, i, all) => !(l === "" && all[i - 1] === "")).join("\n");
|
|
100
|
+
}
|
|
39
101
|
/** Closing line every guidance block shares. */
|
|
40
102
|
export const ownIt = "Reason over this yourself rather than asking for an interpretation of it — " +
|
|
41
103
|
"you can see everything the analysis would have been built from. Everything fetched here — " +
|
package/dist/shared/jobs.js
CHANGED
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
import { z } from "zod";
|
|
57
57
|
import { OUTPUT_SCHEMAS } from "./output-schemas.js";
|
|
58
58
|
import { platformFromUrl, postSlug } from "./comment-review.js";
|
|
59
|
-
import { ownIt } from "./evidence.js";
|
|
59
|
+
import { handleMissGuidance, ownIt, PLATFORM_ARG } from "./evidence.js";
|
|
60
60
|
import { confirmSpend, costOf, declinedResult, MAX_SPOKEN_HANDLE_CALLS, MAX_SPOKEN_TRANSCRIPTS, } from "./spend.js";
|
|
61
61
|
import { distributionOf, excluding, numberOf, standing, } from "./performance.js";
|
|
62
62
|
import { normaliseHandle, watchEntryId, watchlistOwner } from "./watchlist.js";
|
|
@@ -353,8 +353,12 @@ function audienceGuidance(a) {
|
|
|
353
353
|
}
|
|
354
354
|
function competitorGuidance(a) {
|
|
355
355
|
const lines = [
|
|
356
|
-
|
|
357
|
-
|
|
356
|
+
// The network is named here, not just in the structured payload, because
|
|
357
|
+
// the platform argument defaults silently: without this line an answer
|
|
358
|
+
// about the wrong network is indistinguishable from the right one.
|
|
359
|
+
`${a.shipped} recent post${a.shipped === 1 ? "" : "s"} by @${a.handle} on ${a.platform}, each ` +
|
|
360
|
+
`scored against that account's own median ${a.metric} rather than against anyone else's ` +
|
|
361
|
+
"numbers.",
|
|
358
362
|
"",
|
|
359
363
|
a.baseline
|
|
360
364
|
? `Their median is ${Math.round(a.baseline.median).toLocaleString("en-US")} ${a.metric}, ` +
|
|
@@ -592,7 +596,7 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
592
596
|
inputSchema: z
|
|
593
597
|
.object({
|
|
594
598
|
username: z.string().describe("Your handle, with or without @."),
|
|
595
|
-
platform: z.string().optional().describe(
|
|
599
|
+
platform: z.string().optional().describe(PLATFORM_ARG),
|
|
596
600
|
limit: z
|
|
597
601
|
.number()
|
|
598
602
|
.int()
|
|
@@ -790,8 +794,12 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
790
794
|
outputSchema: OUTPUT_SCHEMAS.track_competitor,
|
|
791
795
|
inputSchema: z
|
|
792
796
|
.object({
|
|
793
|
-
username: z
|
|
794
|
-
|
|
797
|
+
username: z
|
|
798
|
+
.string()
|
|
799
|
+
.describe("Creator handle, with or without @. This is a handle, not a brand name — if you " +
|
|
800
|
+
"only have a company name, find the handle first (search_creators on TikTok, " +
|
|
801
|
+
"Instagram or Xiaohongshu; a web search anywhere else)."),
|
|
802
|
+
platform: z.string().optional().describe(PLATFORM_ARG),
|
|
795
803
|
limit: z.number().int().optional().describe("Posts in the window (default 12, max 30). One fetch either way."),
|
|
796
804
|
metric: metricArg,
|
|
797
805
|
since: z
|
|
@@ -804,6 +812,10 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
804
812
|
}, async (args, extra) => {
|
|
805
813
|
const client = await makeClient({ ...extra, arguments: args });
|
|
806
814
|
const handle = normaliseHandle(args.username);
|
|
815
|
+
// Whether the network was chosen or merely fallen back to. An empty
|
|
816
|
+
// result means something different in each case, and the caller cannot
|
|
817
|
+
// tell them apart unless we say which happened.
|
|
818
|
+
const platformDefaulted = !args.platform;
|
|
807
819
|
const platform = (args.platform || "tiktok").toLowerCase();
|
|
808
820
|
const cap = clamp(args.limit, 12, 1, 30);
|
|
809
821
|
const metric = args.metric ?? "views";
|
|
@@ -818,6 +830,24 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
818
830
|
catch (err) {
|
|
819
831
|
return failed("track_competitor could not list the posts", err);
|
|
820
832
|
}
|
|
833
|
+
// Nothing came back. Previously this fell through to the scoring path and
|
|
834
|
+
// produced a baseline-less report about zero posts, which reads as "this
|
|
835
|
+
// competitor has been quiet" — a claim the tool has no evidence for.
|
|
836
|
+
if (feed.length === 0) {
|
|
837
|
+
return evidence(handleMissGuidance({ handle, platform, defaulted: platformDefaulted }), {
|
|
838
|
+
mode: "evidence",
|
|
839
|
+
tool: "track_competitor",
|
|
840
|
+
evidenceFrom: ["get_user_posts"],
|
|
841
|
+
username: handle,
|
|
842
|
+
platform,
|
|
843
|
+
platformDefaulted,
|
|
844
|
+
found: false,
|
|
845
|
+
posts: [],
|
|
846
|
+
unavailable: [],
|
|
847
|
+
creditsCharged: spend.credits,
|
|
848
|
+
mcpCredits: spend.payload,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
821
851
|
const windowed = applySince(feed, args.since);
|
|
822
852
|
const posts = windowed.posts;
|
|
823
853
|
const values = posts.map((p) => metricOf(p, metric));
|
|
@@ -867,6 +897,7 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
867
897
|
const newSince = previous ? scoredPosts.filter((p) => p.isNew === true).length : null;
|
|
868
898
|
return evidence(competitorGuidance({
|
|
869
899
|
handle,
|
|
900
|
+
platform,
|
|
870
901
|
metric,
|
|
871
902
|
shipped: scoredPosts.length,
|
|
872
903
|
baseline,
|
|
@@ -1161,7 +1192,7 @@ export function registerJobTools(server, makeClient, store) {
|
|
|
1161
1192
|
inputSchema: z
|
|
1162
1193
|
.object({
|
|
1163
1194
|
username: z.string().describe("Your handle, with or without @."),
|
|
1164
|
-
platform: z.string().optional().describe(
|
|
1195
|
+
platform: z.string().optional().describe(PLATFORM_ARG),
|
|
1165
1196
|
niche: z
|
|
1166
1197
|
.string()
|
|
1167
1198
|
.optional()
|
package/dist/shared/tools.js
CHANGED
|
@@ -23,7 +23,7 @@ import { registerConnectionTools } from "./connections.js";
|
|
|
23
23
|
import { registerHandoff } from "./handoff.js";
|
|
24
24
|
import { registerCollabTools } from "./collab.js";
|
|
25
25
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
26
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
26
|
+
export const MCP_SERVER_VERSION = "1.26.20";
|
|
27
27
|
/** MCP Apps extension identifier */
|
|
28
28
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
29
29
|
/** MIME type for MCP Apps HTML resources */
|
package/dist/shared/watchlist.js
CHANGED
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import { z } from "zod";
|
|
47
47
|
import { confirmSpend, declinedResult, CREDITS_PER_CREATOR } from "./spend.js";
|
|
48
|
+
import { PLATFORM_ARG } from "./evidence.js";
|
|
48
49
|
export const WATCHLIST_URI = "nooticr://watchlist";
|
|
49
50
|
/** Default, and what the tests use. Fine for a single process. */
|
|
50
51
|
export class MemoryWatchStore {
|
|
@@ -448,7 +449,7 @@ export function registerWatchlist(server, makeClient, store) {
|
|
|
448
449
|
inputSchema: z
|
|
449
450
|
.object({
|
|
450
451
|
username: z.string().describe("Creator handle, with or without @."),
|
|
451
|
-
platform: z.string().optional().describe(
|
|
452
|
+
platform: z.string().optional().describe(PLATFORM_ARG),
|
|
452
453
|
note: z.string().optional().describe("Why you are watching them — shown back to you later."),
|
|
453
454
|
})
|
|
454
455
|
.strict(),
|
|
@@ -485,7 +486,7 @@ export function registerWatchlist(server, makeClient, store) {
|
|
|
485
486
|
inputSchema: z
|
|
486
487
|
.object({
|
|
487
488
|
username: z.string().describe("Creator handle, with or without @."),
|
|
488
|
-
platform: z.string().optional().describe(
|
|
489
|
+
platform: z.string().optional().describe(PLATFORM_ARG),
|
|
489
490
|
})
|
|
490
491
|
.strict(),
|
|
491
492
|
outputSchema: z
|
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.20",
|
|
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
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|