@nooticr/mcp 1.26.44 → 1.26.45
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/fetch-ledger.js +154 -0
- package/dist/shared/output-schemas.js +14 -0
- package/dist/shared/tasks.js +2 -0
- package/dist/shared/tools.js +64 -16
- package/dist/shared/ui-template.js +18 -1
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const LIMIT = 2000;
|
|
2
|
+
const POST_KEYS = ["posts", "window", "demand", "supply", "results"];
|
|
3
|
+
/** One spelling per post: no trailing slash, no tracking query, YouTube's id kept. */
|
|
4
|
+
export function postKey(url) {
|
|
5
|
+
if (typeof url !== "string" || !url.trim())
|
|
6
|
+
return "";
|
|
7
|
+
const raw = url.trim();
|
|
8
|
+
try {
|
|
9
|
+
const u = new URL(raw);
|
|
10
|
+
const host = u.hostname.toLowerCase().replace(/^(www|m|mobile)\./, "");
|
|
11
|
+
const keep = host.endsWith("youtube.com") && u.searchParams.get("v") ? `?v=${u.searchParams.get("v")}` : "";
|
|
12
|
+
return `${host}${u.pathname.replace(/\/+$/, "")}${keep}`;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return raw.replace(/[?#].*$/, "").replace(/\/+$/, "").toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const urlsOf = (row) => [row.externalUrl, row.url, row.permalink, row.postUrl].map(postKey).filter(Boolean);
|
|
19
|
+
const handleOf = (row) => String(row.handle ?? row.username ?? row.creatorHandle ?? "")
|
|
20
|
+
.trim()
|
|
21
|
+
.replace(/^@/, "")
|
|
22
|
+
.toLowerCase();
|
|
23
|
+
export const creatorKey = (row) => {
|
|
24
|
+
const h = handleOf(row);
|
|
25
|
+
return h ? `${String(row.platform ?? "").toLowerCase()}:${h}` : "";
|
|
26
|
+
};
|
|
27
|
+
const runKey = (term, ranAt) => `${String(term ?? "").trim().toLowerCase()}|${String(ranAt ?? "")}`;
|
|
28
|
+
class Bounded {
|
|
29
|
+
map = new Map();
|
|
30
|
+
set(k, v) {
|
|
31
|
+
if (!k)
|
|
32
|
+
return;
|
|
33
|
+
this.map.delete(k);
|
|
34
|
+
this.map.set(k, v);
|
|
35
|
+
if (this.map.size > LIMIT)
|
|
36
|
+
this.map.delete(this.map.keys().next().value);
|
|
37
|
+
}
|
|
38
|
+
get(k) {
|
|
39
|
+
return k ? this.map.get(k) : undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export class FetchLedger {
|
|
43
|
+
posts = new Bounded();
|
|
44
|
+
creators = new Bounded();
|
|
45
|
+
runs = new Bounded();
|
|
46
|
+
/** Keep what a tool returned. The views' own results are never kept: they echo the model. */
|
|
47
|
+
record(tool, structured) {
|
|
48
|
+
if (tool.startsWith("show_") || !structured || typeof structured !== "object")
|
|
49
|
+
return;
|
|
50
|
+
const sc = structured;
|
|
51
|
+
const keepPost = (p) => {
|
|
52
|
+
if (!p || typeof p !== "object")
|
|
53
|
+
return;
|
|
54
|
+
for (const k of urlsOf(p))
|
|
55
|
+
this.posts.set(k, p);
|
|
56
|
+
};
|
|
57
|
+
for (const key of POST_KEYS) {
|
|
58
|
+
if (Array.isArray(sc[key]))
|
|
59
|
+
for (const p of sc[key])
|
|
60
|
+
keepPost(p);
|
|
61
|
+
}
|
|
62
|
+
keepPost(sc.post);
|
|
63
|
+
// analyze_post and its siblings carry the post's fields at the top level.
|
|
64
|
+
if (urlsOf(sc).length && (sc.views != null || sc.caption != null || sc.title != null))
|
|
65
|
+
keepPost(sc);
|
|
66
|
+
if (Array.isArray(sc.creators)) {
|
|
67
|
+
for (const c of sc.creators) {
|
|
68
|
+
if (c && typeof c === "object")
|
|
69
|
+
this.creators.set(creatorKey(c), c);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(sc.runs)) {
|
|
73
|
+
for (const r of sc.runs) {
|
|
74
|
+
if (!r || typeof r !== "object" || r.ranAt == null)
|
|
75
|
+
continue;
|
|
76
|
+
this.runs.set(runKey(sc.term, r.ranAt), r);
|
|
77
|
+
this.runs.set(runKey("", r.ranAt), r);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
post(url) {
|
|
82
|
+
return this.posts.get(postKey(url));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Posts as the view should draw them: nooticr's fields over the model's,
|
|
86
|
+
* so a copied stat can never win, and the model's extra fields survive.
|
|
87
|
+
*/
|
|
88
|
+
checkPosts(sent) {
|
|
89
|
+
const unmatched = [];
|
|
90
|
+
const rows = sent.map((p) => {
|
|
91
|
+
const ours = urlsOf(p).map((k) => this.posts.get(k)).find(Boolean);
|
|
92
|
+
if (!ours) {
|
|
93
|
+
unmatched.push(String(p.externalUrl ?? p.url ?? "a post with no URL"));
|
|
94
|
+
return p;
|
|
95
|
+
}
|
|
96
|
+
return { ...p, ...ours, verified: true };
|
|
97
|
+
});
|
|
98
|
+
return { rows, verification: verdict(rows.length, unmatched, "post") };
|
|
99
|
+
}
|
|
100
|
+
checkCreators(sent) {
|
|
101
|
+
const unmatched = [];
|
|
102
|
+
const rows = sent.map((c) => {
|
|
103
|
+
const ours = this.creators.get(creatorKey(c));
|
|
104
|
+
if (!ours) {
|
|
105
|
+
unmatched.push(`@${handleOf(c) || "?"}${c.platform ? ` on ${String(c.platform)}` : ""}`);
|
|
106
|
+
return c;
|
|
107
|
+
}
|
|
108
|
+
// The order and any ranking note are the model's; every figure is ours.
|
|
109
|
+
return { ...c, ...ours, verified: true };
|
|
110
|
+
});
|
|
111
|
+
return { rows, verification: verdict(rows.length, unmatched, "creator") };
|
|
112
|
+
}
|
|
113
|
+
checkRuns(sent, term) {
|
|
114
|
+
const unmatched = [];
|
|
115
|
+
const rows = sent.map((r) => {
|
|
116
|
+
const ours = this.runs.get(runKey(term, r.ranAt)) ?? this.runs.get(runKey("", r.ranAt));
|
|
117
|
+
if (!ours) {
|
|
118
|
+
unmatched.push(String(r.ranAt ?? "an undated point"));
|
|
119
|
+
return r;
|
|
120
|
+
}
|
|
121
|
+
return { ...r, ...ours, verified: true };
|
|
122
|
+
});
|
|
123
|
+
return { rows, verification: verdict(rows.length, unmatched, "point") };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function verdict(total, unmatched, noun) {
|
|
127
|
+
if (!unmatched.length) {
|
|
128
|
+
return {
|
|
129
|
+
status: "verified",
|
|
130
|
+
note: `All ${total} ${noun}${total === 1 ? "" : "s"} drawn with the figures nooticr returned in this session.`,
|
|
131
|
+
unmatched,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const n = unmatched.length;
|
|
135
|
+
return {
|
|
136
|
+
status: "unverified",
|
|
137
|
+
note: `${n} of ${total} ${noun}${total === 1 ? "" : "s"} ${n === 1 ? "is" : "are"} not among what nooticr ` +
|
|
138
|
+
`returned in this session, so ${n === 1 ? "its" : "their"} figures are as re-sent and were not ` +
|
|
139
|
+
`checked: ${unmatched.slice(0, 5).join(", ")}${n > 5 ? ", …" : ""}.`,
|
|
140
|
+
unmatched,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* One ledger per server. Keyed off the server object so the task path
|
|
145
|
+
* (registerSlowTool) and the plain path record into the same one without
|
|
146
|
+
* threading it through every registration.
|
|
147
|
+
*/
|
|
148
|
+
const LEDGERS = new WeakMap();
|
|
149
|
+
export function ledgerFor(server) {
|
|
150
|
+
let l = LEDGERS.get(server);
|
|
151
|
+
if (!l)
|
|
152
|
+
LEDGERS.set(server, (l = new FetchLedger()));
|
|
153
|
+
return l;
|
|
154
|
+
}
|
|
@@ -311,6 +311,15 @@ const marketplaceScanShape = {
|
|
|
311
311
|
};
|
|
312
312
|
const marketplaceScan = open(marketplaceScanShape);
|
|
313
313
|
const amazonScan = open(amazonScanShape);
|
|
314
|
+
/**
|
|
315
|
+
* Whether a show_* view drew nooticr's own figures (#107): the session's
|
|
316
|
+
* fetch ledger overlays what was actually returned on what the model sent.
|
|
317
|
+
*/
|
|
318
|
+
const checkedAgainstSession = open({
|
|
319
|
+
status: scalar().describe("verified: every row matched what nooticr returned this session. unverified: some are as re-sent."),
|
|
320
|
+
note: scalar(),
|
|
321
|
+
unmatched: listOf(z.string()).describe("Rows not among what nooticr returned in this session; drawn as sent, unchecked."),
|
|
322
|
+
}).nullish();
|
|
314
323
|
export const OUTPUT_SCHEMAS = {
|
|
315
324
|
analyze_post: open({ ...analyzed, ...evidence }),
|
|
316
325
|
analyze_post_fast: open({ ...evidence, ...analyzed }),
|
|
@@ -539,6 +548,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
539
548
|
// to land except chat text. Free, and make no requests, same as
|
|
540
549
|
// show_comment_review: they only draw what they're handed.
|
|
541
550
|
show_compared_posts: open({
|
|
551
|
+
verification: checkedAgainstSession,
|
|
542
552
|
posts: listOf(post).describe("The 2-5 posts being compared, same shape as compare_posts returned."),
|
|
543
553
|
comparison: open({
|
|
544
554
|
winner: scalar().describe("1-indexed position of the post that won, matching the posts array."),
|
|
@@ -550,6 +560,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
550
560
|
mcpCredits,
|
|
551
561
|
}),
|
|
552
562
|
show_post_analysis: open({
|
|
563
|
+
verification: checkedAgainstSession,
|
|
553
564
|
url: scalar(),
|
|
554
565
|
post: post.nullish().describe("The post analyze_post/analyze_post_fast/understand_social_post handed back."),
|
|
555
566
|
analysis: open({}).passthrough().nullish().describe("Your own analysis, any of the fields analyze_post's guidance asked for — summary, " +
|
|
@@ -569,6 +580,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
569
580
|
mcpCredits,
|
|
570
581
|
}),
|
|
571
582
|
show_variants: open({
|
|
583
|
+
verification: checkedAgainstSession,
|
|
572
584
|
sourceUrl: scalar(),
|
|
573
585
|
post: post.nullish(),
|
|
574
586
|
variants: listOf(open({
|
|
@@ -945,6 +957,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
945
957
|
mcpCredits,
|
|
946
958
|
}),
|
|
947
959
|
show_standings: open({
|
|
960
|
+
verification: checkedAgainstSession,
|
|
948
961
|
creators: listOf(anyObject()),
|
|
949
962
|
metric: scalar(),
|
|
950
963
|
ranking: scalar().describe("Which axis the order is on — 'how often' and 'how big' disagree."),
|
|
@@ -1148,6 +1161,7 @@ export const OUTPUT_SCHEMAS = {
|
|
|
1148
1161
|
* might want is left underived on purpose — the model reads the series.
|
|
1149
1162
|
*/
|
|
1150
1163
|
show_trend: open({
|
|
1164
|
+
verification: checkedAgainstSession,
|
|
1151
1165
|
points: listOf(anyObject()),
|
|
1152
1166
|
term: scalar(),
|
|
1153
1167
|
metric: scalar(),
|
package/dist/shared/tasks.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* The task APIs are experimental in SDK 1.30 and may change under us. They are
|
|
25
25
|
* contained here for that reason — one file to fix rather than three handlers.
|
|
26
26
|
*/
|
|
27
|
+
import { ledgerFor } from "./fetch-ledger.js";
|
|
27
28
|
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
28
29
|
/** How long a finished result stays fetchable. */
|
|
29
30
|
export const TASK_TTL_MS = 900_000;
|
|
@@ -67,6 +68,7 @@ export function registerSlowTool(server, name, config, run) {
|
|
|
67
68
|
void (async () => {
|
|
68
69
|
try {
|
|
69
70
|
const result = await run(args, extra);
|
|
71
|
+
ledgerFor(server).record(name, result?.structuredContent);
|
|
70
72
|
await extra.taskStore.storeTaskResult(task.taskId, result?.isError ? "failed" : "completed", result);
|
|
71
73
|
}
|
|
72
74
|
catch (err) {
|
package/dist/shared/tools.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* file for the CLI, KV session for the worker) — the tool bodies, schemas and
|
|
6
6
|
* result formatting live here and nowhere else.
|
|
7
7
|
*/
|
|
8
|
+
import { ledgerFor } from "./fetch-ledger.js";
|
|
8
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
import { NooticrError, safeCreditsHint } from "./nooticr.js";
|
|
@@ -29,7 +30,7 @@ import { registerCollabTools } from "./collab.js";
|
|
|
29
30
|
import { registerAmazonTools } from "./amazon.js";
|
|
30
31
|
import { registerMarketplaceTools } from "./marketplace.js";
|
|
31
32
|
/** Current MCP server version — bumped on every deploy for traceability. */
|
|
32
|
-
export const MCP_SERVER_VERSION = "1.26.
|
|
33
|
+
export const MCP_SERVER_VERSION = "1.26.45";
|
|
33
34
|
/** MCP Apps extension identifier */
|
|
34
35
|
const UI_EXTENSION = "io.modelcontextprotocol/ui";
|
|
35
36
|
/** MIME type for MCP Apps HTML resources */
|
|
@@ -556,6 +557,18 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
556
557
|
},
|
|
557
558
|
},
|
|
558
559
|
});
|
|
560
|
+
// Every tool result passes through the session's fetch ledger on its way
|
|
561
|
+
// out, so the show_* views can draw what nooticr returned rather than the
|
|
562
|
+
// model's copy of it (#107). Wrapped once here rather than in eighty
|
|
563
|
+
// handlers; registerSlowTool records the task path the same way.
|
|
564
|
+
const ledger = ledgerFor(server);
|
|
565
|
+
const registerTool = server.registerTool.bind(server);
|
|
566
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
567
|
+
server.registerTool = (name, config, handler) => registerTool(name, config, async (...a) => {
|
|
568
|
+
const result = (await handler(...a));
|
|
569
|
+
ledger.record(name, result?.structuredContent);
|
|
570
|
+
return result;
|
|
571
|
+
});
|
|
559
572
|
// Register one UI app resource per tool/view. Claude/ChatGPT render a
|
|
560
573
|
// separate sandboxed app per resourceUri and key app state by it, so a
|
|
561
574
|
// distinct URI per tool avoids a shared app instance/session colliding
|
|
@@ -1685,7 +1698,9 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1685
1698
|
// it (show_hooks, show_variants, show_repurposed_post).
|
|
1686
1699
|
server.registerTool("show_trend", {
|
|
1687
1700
|
title: "Show Trend",
|
|
1688
|
-
description: "
|
|
1701
|
+
description: "Figures are checked against what nooticr returned in this session: its own numbers are drawn, " +
|
|
1702
|
+
"and a row it never returned is drawn marked as unchecked, so there is no need to re-type them exactly. " +
|
|
1703
|
+
"Draw the trend you read out of mention_trend. Free, and makes no requests — it renders what " +
|
|
1689
1704
|
"you pass it: the series as a chart, per-network lines where you supply them, and your own " +
|
|
1690
1705
|
"read of what changed. Pass `tooShort` when the series has too few points to call a " +
|
|
1691
1706
|
"direction, and `edgeIsRecordStart` when the left edge is where the record begins rather " +
|
|
@@ -1726,16 +1741,19 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1726
1741
|
})
|
|
1727
1742
|
.strict(),
|
|
1728
1743
|
}, async (args) => {
|
|
1744
|
+
const checked = ledger.checkRuns(args.points, args.term);
|
|
1729
1745
|
return {
|
|
1730
1746
|
content: [
|
|
1731
1747
|
{
|
|
1732
1748
|
type: "text",
|
|
1733
1749
|
text: `Showing a ${args.points.length}-point trend${args.term ? ` for "${args.term}"` : ""}.` +
|
|
1734
|
-
(args.verdict ? ` ${args.verdict}` : "")
|
|
1750
|
+
(args.verdict ? ` ${args.verdict}` : "") +
|
|
1751
|
+
` ${checked.verification.note}`,
|
|
1735
1752
|
},
|
|
1736
1753
|
],
|
|
1737
1754
|
structuredContent: {
|
|
1738
|
-
points:
|
|
1755
|
+
points: checked.rows,
|
|
1756
|
+
verification: checked.verification,
|
|
1739
1757
|
term: args.term ?? null,
|
|
1740
1758
|
metric: args.metric ?? "found",
|
|
1741
1759
|
verdict: args.verdict ?? null,
|
|
@@ -1747,7 +1765,9 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1747
1765
|
});
|
|
1748
1766
|
server.registerTool("show_standings", {
|
|
1749
1767
|
title: "Show Standings",
|
|
1750
|
-
description: "
|
|
1768
|
+
description: "Figures are checked against what nooticr returned in this session: its own numbers are drawn, " +
|
|
1769
|
+
"and a row it never returned is drawn marked as unchecked, so there is no need to re-type them exactly. " +
|
|
1770
|
+
"Draw the standings you read out of compare_creators or watchlist_standings. Free, and makes " +
|
|
1751
1771
|
"no requests — it only renders what you pass it: one row per creator with their window, their " +
|
|
1752
1772
|
"own median, how often they beat it and how hard, and the post that did best. Pass `ranking` " +
|
|
1753
1773
|
"to say which axis you ordered on, and `tooThin` for the creators whose window was too short " +
|
|
@@ -1783,16 +1803,19 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1783
1803
|
})
|
|
1784
1804
|
.strict(),
|
|
1785
1805
|
}, async (args) => {
|
|
1806
|
+
const checked = ledger.checkCreators(args.creators);
|
|
1786
1807
|
return {
|
|
1787
1808
|
content: [
|
|
1788
1809
|
{
|
|
1789
1810
|
type: "text",
|
|
1790
1811
|
text: `Showing standings for ${args.creators.length} creator${args.creators.length === 1 ? "" : "s"}.` +
|
|
1791
|
-
(args.verdict ? ` ${args.verdict}` : "")
|
|
1812
|
+
(args.verdict ? ` ${args.verdict}` : "") +
|
|
1813
|
+
` ${checked.verification.note}`,
|
|
1792
1814
|
},
|
|
1793
1815
|
],
|
|
1794
1816
|
structuredContent: {
|
|
1795
|
-
creators:
|
|
1817
|
+
creators: checked.rows,
|
|
1818
|
+
verification: checked.verification,
|
|
1796
1819
|
metric: args.metric ?? "views",
|
|
1797
1820
|
ranking: args.ranking ?? null,
|
|
1798
1821
|
verdict: args.verdict ?? null,
|
|
@@ -1803,7 +1826,9 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1803
1826
|
});
|
|
1804
1827
|
server.registerTool("show_compared_posts", {
|
|
1805
1828
|
title: "Show Comparison",
|
|
1806
|
-
description: "
|
|
1829
|
+
description: "Figures are checked against what nooticr returned in this session: its own numbers are drawn, " +
|
|
1830
|
+
"and a row it never returned is drawn marked as unchecked, so there is no need to re-type them exactly. " +
|
|
1831
|
+
"Display a comparison you wrote after compare_posts fetched the first post and you fetched " +
|
|
1807
1832
|
"the rest yourself (get_social_media, 1 credit each). Free, and makes no requests — it only " +
|
|
1808
1833
|
"draws what you pass it: each post with a BEST badge on the winner, what differed, shared " +
|
|
1809
1834
|
"strengths and the next experiment worth running. Call this after you have done the " +
|
|
@@ -1834,15 +1859,18 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1834
1859
|
})
|
|
1835
1860
|
.strict(),
|
|
1836
1861
|
}, async (args) => {
|
|
1862
|
+
const checked = ledger.checkPosts(args.posts);
|
|
1837
1863
|
return {
|
|
1838
1864
|
content: [
|
|
1839
1865
|
{
|
|
1840
1866
|
type: "text",
|
|
1841
|
-
text: `Showing a comparison of ${args.posts.length} posts.${args.winnerReason ? ` ${args.winnerReason}` : ""}
|
|
1867
|
+
text: `Showing a comparison of ${args.posts.length} posts.${args.winnerReason ? ` ${args.winnerReason}` : ""}` +
|
|
1868
|
+
` ${checked.verification.note}`,
|
|
1842
1869
|
},
|
|
1843
1870
|
],
|
|
1844
1871
|
structuredContent: {
|
|
1845
|
-
posts:
|
|
1872
|
+
posts: checked.rows,
|
|
1873
|
+
verification: checked.verification,
|
|
1846
1874
|
comparison: {
|
|
1847
1875
|
winner: args.winner,
|
|
1848
1876
|
winnerReason: args.winnerReason ?? null,
|
|
@@ -1856,7 +1884,9 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1856
1884
|
});
|
|
1857
1885
|
server.registerTool("show_post_analysis", {
|
|
1858
1886
|
title: "Show Analysis",
|
|
1859
|
-
description: "
|
|
1887
|
+
description: "Figures are checked against what nooticr returned in this session: its own numbers are drawn, " +
|
|
1888
|
+
"and a row it never returned is drawn marked as unchecked, so there is no need to re-type them exactly. " +
|
|
1889
|
+
"Display an analysis you wrote after analyze_post, analyze_post_fast or understand_social_post " +
|
|
1860
1890
|
"handed you the material. Free, and makes no requests — it only draws what you pass it: hook " +
|
|
1861
1891
|
"strength, script structure, quotable lines, hashtags, target audience, viral triggers and " +
|
|
1862
1892
|
"more, whichever of these you actually produced. Call this after you have done the analysing, " +
|
|
@@ -1883,11 +1913,15 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1883
1913
|
})
|
|
1884
1914
|
.strict(),
|
|
1885
1915
|
}, async (args) => {
|
|
1916
|
+
const checked = ledger.checkPosts([
|
|
1917
|
+
{ ...(args.post ?? { platform: platformFromUrl(args.url) }), externalUrl: args.post?.externalUrl ?? args.url },
|
|
1918
|
+
]);
|
|
1886
1919
|
return {
|
|
1887
|
-
content: [{ type: "text", text: `Showing your analysis of ${args.url}
|
|
1920
|
+
content: [{ type: "text", text: `Showing your analysis of ${args.url}. ${checked.verification.note}` }],
|
|
1888
1921
|
structuredContent: {
|
|
1889
1922
|
url: args.url,
|
|
1890
|
-
post:
|
|
1923
|
+
post: checked.rows[0],
|
|
1924
|
+
verification: checked.verification,
|
|
1891
1925
|
analysis: args.analysis,
|
|
1892
1926
|
mcpCredits: { cost: 0 },
|
|
1893
1927
|
},
|
|
@@ -1932,7 +1966,9 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1932
1966
|
});
|
|
1933
1967
|
server.registerTool("show_variants", {
|
|
1934
1968
|
title: "Show Variants",
|
|
1935
|
-
description: "
|
|
1969
|
+
description: "Figures are checked against what nooticr returned in this session: its own numbers are drawn, " +
|
|
1970
|
+
"and a row it never returned is drawn marked as unchecked, so there is no need to re-type them exactly. " +
|
|
1971
|
+
"Display the post variants you wrote after create_variants handed you the original post's " +
|
|
1936
1972
|
"material. Free, and makes no requests — it only draws what you pass it: each variant's hook, " +
|
|
1937
1973
|
"the angle that changes, its shot beats and its call to action. Call this after you have " +
|
|
1938
1974
|
"written the variants, not instead of writing them.",
|
|
@@ -1960,11 +1996,23 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1960
1996
|
})
|
|
1961
1997
|
.strict(),
|
|
1962
1998
|
}, async (args) => {
|
|
1999
|
+
const checked = ledger.checkPosts([
|
|
2000
|
+
{
|
|
2001
|
+
...(args.post ?? { platform: platformFromUrl(args.sourceUrl) }),
|
|
2002
|
+
externalUrl: args.post?.externalUrl ?? args.sourceUrl,
|
|
2003
|
+
},
|
|
2004
|
+
]);
|
|
1963
2005
|
return {
|
|
1964
|
-
content: [
|
|
2006
|
+
content: [
|
|
2007
|
+
{
|
|
2008
|
+
type: "text",
|
|
2009
|
+
text: `Showing ${args.variants.length} variants of ${args.sourceUrl}. ${checked.verification.note}`,
|
|
2010
|
+
},
|
|
2011
|
+
],
|
|
1965
2012
|
structuredContent: {
|
|
1966
2013
|
sourceUrl: args.sourceUrl,
|
|
1967
|
-
post:
|
|
2014
|
+
post: checked.rows[0],
|
|
2015
|
+
verification: checked.verification,
|
|
1968
2016
|
variants: args.variants,
|
|
1969
2017
|
mcpCredits: { cost: 0 },
|
|
1970
2018
|
},
|
|
@@ -3464,7 +3464,24 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
|
|
|
3464
3464
|
// after it — one wrapper instead of a call at each of the many early
|
|
3465
3465
|
// returns below.
|
|
3466
3466
|
function render(result){
|
|
3467
|
-
try{renderView(result);}finally{initPlayers();setTimeout(reportSize,80);}
|
|
3467
|
+
try{renderView(result);verificationBanner(result);}finally{initPlayers();setTimeout(reportSize,80);}
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3470
|
+
// #107: a show_* view whose rows could not all be matched to what nooticr
|
|
3471
|
+
// returned this session says so above the card. The marketplace card draws
|
|
3472
|
+
// its own. Only the unverified case is drawn: a banner on every honest card
|
|
3473
|
+
// would be noise that trains a reader to skip it.
|
|
3474
|
+
function verificationBanner(result){
|
|
3475
|
+
var d=extractResult(result);
|
|
3476
|
+
if(!d||typeof d!=="object"||d.marketplace)return;
|
|
3477
|
+
var v=d.verification;
|
|
3478
|
+
if(!v||v.status!=="unverified")return;
|
|
3479
|
+
var app=document.getElementById("app");if(!app)return;
|
|
3480
|
+
var b=document.createElement("div");
|
|
3481
|
+
b.setAttribute("data-verification","unverified");
|
|
3482
|
+
b.style.cssText="font-size:12px;padding:8px 10px;margin:0 0 10px;border-radius:8px;border:1px solid var(--border);background:rgba(180,35,24,.06)";
|
|
3483
|
+
b.innerHTML="<b>Not checked against nooticr</b> · "+esc(v.note||"");
|
|
3484
|
+
app.insertBefore(b,app.firstChild);
|
|
3468
3485
|
}
|
|
3469
3486
|
|
|
3470
3487
|
// ─── Own account ───
|
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.45",
|
|
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": {
|