@nooticr/mcp 1.26.29 → 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/dist/shared/brand-watch.js +20 -4
- package/dist/shared/evidence-digest.js +269 -0
- package/dist/shared/jobs.js +15 -7
- package/dist/shared/own-account.js +12 -2
- package/dist/shared/tools.js +39 -17
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* rather than a build.
|
|
14
14
|
*/
|
|
15
15
|
import { z } from "zod";
|
|
16
|
+
import { withEvidence } from "./evidence-digest.js";
|
|
16
17
|
import { OUTPUT_SCHEMAS } from "./output-schemas.js";
|
|
17
18
|
import { viewMeta } from "./view-meta.js";
|
|
18
19
|
import { BACKEND_CALL_CREDITS, confirmSpend, declinedResult, searchMentionsCost } from "./spend.js";
|
|
@@ -268,9 +269,17 @@ function trendGuidance(sc, runs) {
|
|
|
268
269
|
}
|
|
269
270
|
function toResult(proxy) {
|
|
270
271
|
const textBlock = proxy.contentBlocks.find((c) => c.type === "text");
|
|
272
|
+
const structured = proxy.structured;
|
|
273
|
+
// The backend's text block is a one-line summary — "1 active brand watch."
|
|
274
|
+
// — and the watches themselves are in `structuredContent`, which a host
|
|
275
|
+
// rendering the view keeps for the widget. A model asked which watch to stop
|
|
276
|
+
// could see the count and not the terms (#59).
|
|
277
|
+
const text = String(textBlock?.text ?? "");
|
|
271
278
|
return {
|
|
272
|
-
content:
|
|
273
|
-
|
|
279
|
+
content: [
|
|
280
|
+
{ type: "text", text: structured ? withEvidence(text, structured) : text },
|
|
281
|
+
],
|
|
282
|
+
structuredContent: structured,
|
|
274
283
|
};
|
|
275
284
|
}
|
|
276
285
|
function failed(prefix, err) {
|
|
@@ -468,10 +477,17 @@ export function registerBrandWatch(server, makeClient) {
|
|
|
468
477
|
// model reading two points as a trend, and reading the start of the
|
|
469
478
|
// retention window as the moment a conversation began.
|
|
470
479
|
const guidance = trendGuidance(sc, runs);
|
|
480
|
+
const payload = { guidance, ...sc };
|
|
471
481
|
return {
|
|
472
482
|
...result,
|
|
473
|
-
|
|
474
|
-
|
|
483
|
+
// "8 runs of X, newest first" with the runs in the other channel is
|
|
484
|
+
// the same deictic failure as everywhere else (#59): a host that
|
|
485
|
+
// renders the view keeps `structuredContent` for the widget, and the
|
|
486
|
+
// model is left counting runs it cannot read.
|
|
487
|
+
content: [
|
|
488
|
+
{ type: "text", text: withEvidence(guidance, payload) },
|
|
489
|
+
],
|
|
490
|
+
structuredContent: payload,
|
|
475
491
|
};
|
|
476
492
|
}
|
|
477
493
|
catch (err) {
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A compact rendering of a payload's evidence, for the channel that gets the
|
|
3
|
+
* text blocks.
|
|
4
|
+
*
|
|
5
|
+
* ## Why this exists
|
|
6
|
+
*
|
|
7
|
+
* There are two channels out of a tool call and no host delivers both to the
|
|
8
|
+
* model:
|
|
9
|
+
*
|
|
10
|
+
* | | `content` text blocks | `structuredContent` |
|
|
11
|
+
* |---|---|---|
|
|
12
|
+
* | Claude Code | dropped when `structuredContent` is present | delivered, serialised |
|
|
13
|
+
* | Claude.ai, tool with a UI view | delivered | routed to the widget |
|
|
14
|
+
*
|
|
15
|
+
* #44 found the first column and #51 fixed it, by writing the guidance into
|
|
16
|
+
* both channels. The evidence never got the same treatment, so it survived
|
|
17
|
+
* Claude Code and vanished on Claude.ai — where a real session received
|
|
18
|
+
* "3 posts that might be someone describing ...", instructions to quote lines
|
|
19
|
+
* from them and report their permalinks, and not one post (#59).
|
|
20
|
+
*
|
|
21
|
+
* The tools most affected were the composed ones, which replace the backend's
|
|
22
|
+
* text block with their own guidance. That guidance is deictic — "Here are 4
|
|
23
|
+
* comments", "2 creators", "8 runs" — so it counts and describes material that
|
|
24
|
+
* is somewhere else. "Here are 4 comments" is a false sentence in the channel
|
|
25
|
+
* it is written in.
|
|
26
|
+
*
|
|
27
|
+
* So: each channel has to be self-sufficient. This renders the evidence for
|
|
28
|
+
* the text one.
|
|
29
|
+
*
|
|
30
|
+
* ## What it is not
|
|
31
|
+
*
|
|
32
|
+
* Not the serialised payload. A model asked to judge posts needs the caption,
|
|
33
|
+
* the handle and the link; it does not need `engagementRate` to four decimal
|
|
34
|
+
* places, and 14 KB of JSON in a text block crowds out the guidance that says
|
|
35
|
+
* what to do with it. Each shape below renders the fields its guidance
|
|
36
|
+
* actually names.
|
|
37
|
+
*/
|
|
38
|
+
/** Per-item text budget. Long enough for a real Reddit complaint, short enough that twenty fit. */
|
|
39
|
+
const ITEM_CHARS = 600;
|
|
40
|
+
/**
|
|
41
|
+
* Whole-digest budget.
|
|
42
|
+
*
|
|
43
|
+
* `answer_my_audience` can carry 14 KB of payload. The cap is what keeps a
|
|
44
|
+
* text block readable rather than a second serialisation of it — and when it
|
|
45
|
+
* bites, the digest says so, because a model reasoning over 20 of 60 comments
|
|
46
|
+
* has to know it has 20.
|
|
47
|
+
*/
|
|
48
|
+
const DIGEST_CHARS = 6000;
|
|
49
|
+
/** How many items of one kind are worth rendering before the count matters more than the detail. */
|
|
50
|
+
const MAX_ITEMS = 25;
|
|
51
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
52
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
|
|
53
|
+
function clamp(text, max = ITEM_CHARS) {
|
|
54
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
55
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
56
|
+
}
|
|
57
|
+
/** `12.3k`, because a raw 12345 beside four other numbers is harder to read than it looks. */
|
|
58
|
+
function compactCount(n) {
|
|
59
|
+
if (n >= 1_000_000)
|
|
60
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
61
|
+
if (n >= 1_000)
|
|
62
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
63
|
+
return String(n);
|
|
64
|
+
}
|
|
65
|
+
function stats(row) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
const views = num(row.views);
|
|
68
|
+
const likes = num(row.likes);
|
|
69
|
+
const comments = num(row.comments);
|
|
70
|
+
if (views)
|
|
71
|
+
parts.push(`${compactCount(views)} views`);
|
|
72
|
+
if (likes)
|
|
73
|
+
parts.push(`${compactCount(likes)} likes`);
|
|
74
|
+
if (comments)
|
|
75
|
+
parts.push(`${compactCount(comments)} comments`);
|
|
76
|
+
return parts.join(", ");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One post, as a line a model can cite.
|
|
80
|
+
*
|
|
81
|
+
* `externalUrl` is not decoration: `find_people_with_problem`'s guidance asks
|
|
82
|
+
* for qualifying posts to be reported "with their permalink, so they can
|
|
83
|
+
* actually be replied to". A digest without links cannot answer that.
|
|
84
|
+
*/
|
|
85
|
+
function postLine(row) {
|
|
86
|
+
const head = [
|
|
87
|
+
str(row.platform) || null,
|
|
88
|
+
str(row.creatorHandle) ? `@${str(row.creatorHandle)}` : null,
|
|
89
|
+
str(row.subreddit) || null,
|
|
90
|
+
str(row.foundBy) ? `found by: ${str(row.foundBy)}` : null,
|
|
91
|
+
stats(row) || null,
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(" · ");
|
|
95
|
+
const text = clamp(str(row.caption) || str(row.title) || str(row.text));
|
|
96
|
+
const url = str(row.externalUrl) || str(row.url);
|
|
97
|
+
return [head && `[${head}]`, text, url].filter(Boolean).join("\n ");
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One comment, with its id.
|
|
101
|
+
*
|
|
102
|
+
* The id is the load-bearing field: `analyze_comments` asks for a
|
|
103
|
+
* classification per comment and `show_comment_review` takes those ids back.
|
|
104
|
+
* A digest that renders the text and drops the id breaks the next call.
|
|
105
|
+
*/
|
|
106
|
+
function commentLine(row) {
|
|
107
|
+
const id = str(row.id);
|
|
108
|
+
const who = str(row.author) || str(row.username);
|
|
109
|
+
const likes = num(row.likes);
|
|
110
|
+
const head = [id || null, who ? `@${who}` : null, likes ? `${compactCount(likes)} likes` : null]
|
|
111
|
+
.filter(Boolean)
|
|
112
|
+
.join(" · ");
|
|
113
|
+
return [head && `[${head}]`, clamp(str(row.text) || str(row.comment))].filter(Boolean).join("\n ");
|
|
114
|
+
}
|
|
115
|
+
function creatorLine(row) {
|
|
116
|
+
const head = [
|
|
117
|
+
str(row.username) ? `@${str(row.username)}` : str(row.handle) ? `@${str(row.handle)}` : null,
|
|
118
|
+
str(row.platform) || null,
|
|
119
|
+
num(row.followers) ? `${compactCount(num(row.followers))} followers` : null,
|
|
120
|
+
str(row.foundBy) ? `found by: ${str(row.foundBy)}` : null,
|
|
121
|
+
]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(" · ");
|
|
124
|
+
const why = clamp(str(row.why) || str(row.bio) || str(row.verdict));
|
|
125
|
+
const links = Array.isArray(row.links)
|
|
126
|
+
? row.links
|
|
127
|
+
.map((l) => str(l.url))
|
|
128
|
+
.filter(Boolean)
|
|
129
|
+
.slice(0, 3)
|
|
130
|
+
.join(" ")
|
|
131
|
+
: "";
|
|
132
|
+
return [head && `[${head}]`, why, links].filter(Boolean).join("\n ");
|
|
133
|
+
}
|
|
134
|
+
function runLine(row) {
|
|
135
|
+
const when = str(row.ranAt).slice(0, 10);
|
|
136
|
+
const found = num(row.found);
|
|
137
|
+
const reported = num(row.reported);
|
|
138
|
+
return `${when || "(undated)"} · found ${found ?? "?"} · reported ${reported ?? "?"}`;
|
|
139
|
+
}
|
|
140
|
+
/** Everything with no shape of its own: the short scalar fields, in order. */
|
|
141
|
+
function genericLine(row) {
|
|
142
|
+
const pairs = Object.entries(row)
|
|
143
|
+
.filter(([, v]) => typeof v === "string" || typeof v === "number" || typeof v === "boolean")
|
|
144
|
+
.map(([k, v]) => `${k}: ${clamp(String(v), 160)}`)
|
|
145
|
+
.slice(0, 8);
|
|
146
|
+
return pairs.join(" · ");
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The evidence keys worth rendering, and how.
|
|
150
|
+
*
|
|
151
|
+
* Ordered: when a payload carries several, the first is the one the guidance
|
|
152
|
+
* is most likely to be about.
|
|
153
|
+
*/
|
|
154
|
+
const SHAPES = [
|
|
155
|
+
{ key: "posts", label: "post", line: postLine },
|
|
156
|
+
// The baseline a post is scored against, and the asks a niche sweep found.
|
|
157
|
+
// Both are posts in all but name, and both are the material their tool's
|
|
158
|
+
// guidance reasons over — `why_did_this_underperform` cannot say "this is an
|
|
159
|
+
// ordinary result" without the distribution it is ordinary against.
|
|
160
|
+
{ key: "window", label: "post in the baseline window", line: postLine },
|
|
161
|
+
{ key: "demand", label: "thing the audience asked for", line: postLine },
|
|
162
|
+
{ key: "supply", label: "post already serving it", line: postLine },
|
|
163
|
+
{ key: "comments", label: "comment", line: commentLine },
|
|
164
|
+
{ key: "replies", label: "reply", line: commentLine },
|
|
165
|
+
{ key: "mentions", label: "mention", line: commentLine },
|
|
166
|
+
{ key: "creators", label: "creator", line: creatorLine },
|
|
167
|
+
{ key: "candidates", label: "candidate", line: creatorLine },
|
|
168
|
+
{ key: "runs", label: "run", line: runLine },
|
|
169
|
+
{ key: "cues", label: "line", line: genericLine },
|
|
170
|
+
{ key: "segments", label: "segment", line: genericLine },
|
|
171
|
+
{ key: "hashtags", label: "hashtag", line: genericLine },
|
|
172
|
+
{ key: "sounds", label: "sound", line: genericLine },
|
|
173
|
+
{ key: "watches", label: "watch", line: genericLine },
|
|
174
|
+
{ key: "apps", label: "app", line: genericLine },
|
|
175
|
+
{ key: "connections", label: "connection", line: genericLine },
|
|
176
|
+
{ key: "unavailable", label: "network that could not be searched", line: genericLine },
|
|
177
|
+
];
|
|
178
|
+
/**
|
|
179
|
+
* Render whatever evidence a payload carries, or "" when it carries none.
|
|
180
|
+
*
|
|
181
|
+
* Returning "" rather than a placeholder matters: a tool whose result is a
|
|
182
|
+
* verdict rather than material (`show_*`, a state mutation) should get its
|
|
183
|
+
* guidance and nothing appended.
|
|
184
|
+
*/
|
|
185
|
+
/** Fields that identify a row — the ones a caller would cite it by. */
|
|
186
|
+
const IDENTIFYING = [
|
|
187
|
+
"id",
|
|
188
|
+
"externalUrl",
|
|
189
|
+
"url",
|
|
190
|
+
"permalink",
|
|
191
|
+
"caption",
|
|
192
|
+
"text",
|
|
193
|
+
"title",
|
|
194
|
+
"username",
|
|
195
|
+
"handle",
|
|
196
|
+
"creatorHandle",
|
|
197
|
+
"name",
|
|
198
|
+
"term",
|
|
199
|
+
"appId",
|
|
200
|
+
"ranAt",
|
|
201
|
+
"tag",
|
|
202
|
+
];
|
|
203
|
+
/**
|
|
204
|
+
* Any array of objects the named shapes did not claim.
|
|
205
|
+
*
|
|
206
|
+
* A hand-maintained list of keys is exactly the artefact that drifts — a tool
|
|
207
|
+
* adds `window` or `demand`, nobody updates the list, and the material goes
|
|
208
|
+
* missing again with every test still green. So anything that looks like a row
|
|
209
|
+
* with an identity gets rendered, well or plainly, rather than dropped.
|
|
210
|
+
*/
|
|
211
|
+
function unclaimedShapes(payload) {
|
|
212
|
+
const named = new Set(SHAPES.map((s) => s.key));
|
|
213
|
+
return Object.entries(payload)
|
|
214
|
+
.filter(([key, rows]) => {
|
|
215
|
+
if (named.has(key) || !Array.isArray(rows) || rows.length === 0)
|
|
216
|
+
return false;
|
|
217
|
+
const first = rows.find((r) => r && typeof r === "object");
|
|
218
|
+
if (!first)
|
|
219
|
+
return false;
|
|
220
|
+
return IDENTIFYING.some((f) => {
|
|
221
|
+
const v = first[f];
|
|
222
|
+
return typeof v === "string" || typeof v === "number";
|
|
223
|
+
});
|
|
224
|
+
})
|
|
225
|
+
.map(([key]) => ({ key, label: key.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase(), line: genericLine }));
|
|
226
|
+
}
|
|
227
|
+
export function evidenceDigest(payload) {
|
|
228
|
+
const blocks = [];
|
|
229
|
+
let spent = 0;
|
|
230
|
+
for (const shape of [...SHAPES, ...unclaimedShapes(payload)]) {
|
|
231
|
+
const rows = payload[shape.key];
|
|
232
|
+
if (!Array.isArray(rows) || rows.length === 0)
|
|
233
|
+
continue;
|
|
234
|
+
if (spent >= DIGEST_CHARS)
|
|
235
|
+
break;
|
|
236
|
+
const shown = [];
|
|
237
|
+
for (const row of rows.slice(0, MAX_ITEMS)) {
|
|
238
|
+
if (spent >= DIGEST_CHARS)
|
|
239
|
+
break;
|
|
240
|
+
const line = shape.line((row ?? {}));
|
|
241
|
+
if (!line.trim())
|
|
242
|
+
continue;
|
|
243
|
+
shown.push(line);
|
|
244
|
+
spent += line.length;
|
|
245
|
+
}
|
|
246
|
+
if (!shown.length)
|
|
247
|
+
continue;
|
|
248
|
+
// Say what was left out. A model reasoning over 20 of 60 has to know it
|
|
249
|
+
// has 20 — a digest that silently truncates turns "here is everything" into
|
|
250
|
+
// a claim the caller cannot check.
|
|
251
|
+
const total = rows.length;
|
|
252
|
+
const plural = total === 1 || shape.label.endsWith("s") ? shape.label : `${shape.label}s`;
|
|
253
|
+
const header = shown.length < total
|
|
254
|
+
? `${shown.length} of ${total} ${plural} (the rest are in the structured payload and in the view):`
|
|
255
|
+
: `${total} ${plural}:`;
|
|
256
|
+
blocks.push([header, ...shown.map((l, i) => `${i + 1}. ${l}`)].join("\n"));
|
|
257
|
+
}
|
|
258
|
+
return blocks.join("\n\n");
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Guidance and evidence in one text block, in the order a reader needs them.
|
|
262
|
+
*
|
|
263
|
+
* Guidance first: it is the instruction, and a model that reads the material
|
|
264
|
+
* before knowing what it is for reads it twice.
|
|
265
|
+
*/
|
|
266
|
+
export function withEvidence(guidance, payload) {
|
|
267
|
+
const digest = evidenceDigest(payload);
|
|
268
|
+
return digest ? `${guidance}\n\n---\n\n${digest}` : guidance;
|
|
269
|
+
}
|
package/dist/shared/jobs.js
CHANGED
|
@@ -57,6 +57,7 @@ import { z } from "zod";
|
|
|
57
57
|
import { OUTPUT_SCHEMAS } from "./output-schemas.js";
|
|
58
58
|
import { platformFromUrl, postSlug } from "./comment-review.js";
|
|
59
59
|
import { clamp, handleMissGuidance, ownIt, PLATFORM_ARG } from "./evidence.js";
|
|
60
|
+
import { withEvidence } from "./evidence-digest.js";
|
|
60
61
|
import { confirmSpend, costOf, declinedResult, MAX_SPOKEN_HANDLE_CALLS, MAX_SPOKEN_TRANSCRIPTS, SPOKEN_PLATFORMS, CREDITS_PER_CREATOR, } from "./spend.js";
|
|
61
62
|
import { distributionOf, excluding, numberOf, standing, } from "./performance.js";
|
|
62
63
|
import { normaliseHandle, watchEntryId, watchlistOwner } from "./watchlist.js";
|
|
@@ -596,14 +597,21 @@ const metricArg = z
|
|
|
596
597
|
.optional()
|
|
597
598
|
.describe("Which stat to rank on (default views).");
|
|
598
599
|
export function registerJobTools(server, makeClient, store) {
|
|
599
|
-
/** Guidance
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
//
|
|
600
|
+
/** Guidance AND evidence in both channels, because no host delivers both. */
|
|
601
|
+
// Neither channel is safe on its own, and which one is dropped depends on
|
|
602
|
+
// the host: Claude Code discards every `content` text block when a result
|
|
603
|
+
// carries `structuredContent` (#44, measured at 0 guidance phrases across
|
|
604
|
+
// 175 tool results), while Claude.ai routes `structuredContent` to the
|
|
605
|
+
// widget and hands the model only the text blocks (#59).
|
|
606
|
+
//
|
|
607
|
+
// #51 made the guidance survive both. The evidence did not, so a real
|
|
608
|
+
// session got "3 posts that might be someone describing ...", instructions
|
|
609
|
+
// to quote lines from them, and no posts. The guidance these tools write is
|
|
610
|
+
// deictic — it counts and describes material that used to be somewhere
|
|
611
|
+
// else — so `withEvidence` puts a rendering of that material in the same
|
|
612
|
+
// block as the sentence describing it.
|
|
605
613
|
const evidence = (guidance, payload) => ({
|
|
606
|
-
content: [{ type: "text", text: guidance }],
|
|
614
|
+
content: [{ type: "text", text: withEvidence(guidance, payload) }],
|
|
607
615
|
structuredContent: { guidance, ...payload },
|
|
608
616
|
});
|
|
609
617
|
const failed = (prefix, err) => ({
|
|
@@ -51,13 +51,23 @@
|
|
|
51
51
|
* `get_content_plan` above); this file is what makes them reachable here.
|
|
52
52
|
*/
|
|
53
53
|
import { z } from "zod";
|
|
54
|
+
import { withEvidence } from "./evidence-digest.js";
|
|
54
55
|
import { OUTPUT_SCHEMAS } from "./output-schemas.js";
|
|
55
56
|
import { viewMeta } from "./view-meta.js";
|
|
56
57
|
function toResult(proxy) {
|
|
57
58
|
const textBlock = proxy.contentBlocks.find((c) => c.type === "text");
|
|
59
|
+
const structured = proxy.structured;
|
|
60
|
+
// The backend's summary line, plus the rows it summarises. `list_own_apps`
|
|
61
|
+
// said "You have 2 app(s) in this workspace" and kept the appIds in
|
|
62
|
+
// `structuredContent` — which a host rendering the view gives to the widget,
|
|
63
|
+
// leaving a model that needs an appId for draft_post with the count alone
|
|
64
|
+
// (#59).
|
|
65
|
+
const text = String(textBlock?.text ?? "");
|
|
58
66
|
return {
|
|
59
|
-
content:
|
|
60
|
-
|
|
67
|
+
content: [
|
|
68
|
+
{ type: "text", text: structured ? withEvidence(text, structured) : text },
|
|
69
|
+
],
|
|
70
|
+
structuredContent: structured,
|
|
61
71
|
};
|
|
62
72
|
}
|
|
63
73
|
function failed(prefix, err) {
|
package/dist/shared/tools.js
CHANGED
|
@@ -15,6 +15,7 @@ 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
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 */
|
|
@@ -444,6 +445,19 @@ async function runEvidence(tool, args, client) {
|
|
|
444
445
|
// rather than "no fetch was made".
|
|
445
446
|
if (plan.also && !nothingToFetch)
|
|
446
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
|
+
}
|
|
447
461
|
return { content, structuredContent: out };
|
|
448
462
|
}
|
|
449
463
|
export function createMcpServer(rawMakeClient, opts) {
|
|
@@ -1202,23 +1216,31 @@ export function createMcpServer(rawMakeClient, opts) {
|
|
|
1202
1216
|
const structured = (res.structured ?? {});
|
|
1203
1217
|
const comments = toEvidence(args.url, structured.comments);
|
|
1204
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
|
+
};
|
|
1205
1239
|
return {
|
|
1206
|
-
content: [
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
// chain hangs off.
|
|
1211
|
-
guidance,
|
|
1212
|
-
mode: "evidence",
|
|
1213
|
-
url: args.url,
|
|
1214
|
-
platform: structured.platform ?? null,
|
|
1215
|
-
commentCount: comments.length,
|
|
1216
|
-
comments,
|
|
1217
|
-
// Passed through rather than re-derived: the platform's own clustering
|
|
1218
|
-
// is evidence too, and it is already paid for.
|
|
1219
|
-
themes: structured.themes ?? [],
|
|
1220
|
-
mcpCredits: structured.mcpCredits ?? null,
|
|
1221
|
-
},
|
|
1240
|
+
content: [
|
|
1241
|
+
{ type: "text", text: `${guidance}\n\n---\n\n${evidenceDigest(payload)}` },
|
|
1242
|
+
],
|
|
1243
|
+
structuredContent: payload,
|
|
1222
1244
|
};
|
|
1223
1245
|
}
|
|
1224
1246
|
catch (err) {
|
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": {
|