@nooticr/mcp 1.26.54 → 1.26.56

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 CHANGED
@@ -52,7 +52,7 @@ npx @nooticr/mcp login # one-time sign-in (Google)
52
52
 
53
53
  ## Tools
54
54
 
55
- 88 tools, grouped by what you are trying to do. Prices are in nooticr credits and
55
+ 90 tools, grouped by what you are trying to do. Prices are in nooticr credits and
56
56
  match what the server actually charges.
57
57
 
58
58
  The job tools under **Answer a question you actually have** are not
@@ -78,11 +78,11 @@ them caps that fan-out with an argument.
78
78
  | `analyze_post_fast` | 2 | The post's transcript, caption and stats — everything but the pictures, which is what makes it the cheap read. Two fetches: `get_social_media` (1) and `get_post_transcript` (1). The sensible default. |
79
79
  | `analyze_post` | 3 | Frames sampled across the video, as **images your model can actually look at**, plus the transcript. Two fetches: `get_post_frames` (2) and `get_post_transcript` (1). Use when the visuals are the point — framing, editing, on-screen text. |
80
80
  | `understand_social_post` | 3 | The same two fetches, asked for a description of what physically happens on screen rather than why it works. Use when you need the events, not the strategy. |
81
- | `analyze_comments` | 2 | The comment section, every comment with a stable id, and the taxonomy to label them with — sentiment, and whether each is praise, a complaint, a bug report, a question, a request, a comparison or spam. The same `get_post_comments` call, at the same price as reading them directly. Pass `verbatim: true` to have every item rendered whole for quoting rather than clipped to fit; same price. |
81
+ | `summarize_post_comments` | 2 | The comment section, every comment with a stable id, and the taxonomy to label them with — sentiment, and whether each is praise, a complaint, a bug report, a question, a request, a comparison or spam. The same `get_post_comments` call, at the same price as reading them directly. Pass `verbatim: true` to have every item rendered whole for quoting rather than clipped to fit; same price. |
82
82
  | `show_comment_review` | free | Draws the classifications your model produced — every comment with its sentiment and category, filterable and selectable. Makes no requests; it only renders what you pass it. |
83
83
  | `show_post_analysis` | free | Draws the analysis your model wrote from `analyze_post`/`analyze_post_fast`/`understand_social_post` — hook strength, script structure, quotable lines, suggested hashtags and more, whichever fields you produced. Makes no requests; it only renders what you pass it. |
84
- | `compare_posts` | 1 | The first of two to five posts, fetched with its stats, and the comparison left to you. Fetch the rest with `get_social_media` at 1 credit each. Use when performance differs and you need to know why. |
85
- | `show_compared_posts` | free | Draws the comparison your model wrote from `compare_posts` — each post scored, the winner marked, what differed and the next test worth running. Makes no requests; it only renders what you pass it. |
84
+ | `compare_social_posts` | 1 | The first of two to five posts, fetched with its stats, and the comparison left to you. Fetch the rest with `get_social_media` at 1 credit each. Use when performance differs and you need to know why. |
85
+ | `show_compared_posts` | free | Draws the comparison your model wrote from `compare_social_posts` — each post scored, the winner marked, what differed and the next test worth running. Makes no requests; it only renders what you pass it. |
86
86
 
87
87
  ### Research a niche or a creator
88
88
 
@@ -124,7 +124,7 @@ them caps that fan-out with an argument.
124
124
  | `who_should_i_work_with` | 2, or 4 with a seed | A collaboration shortlist: a keyword search merged with the lookalikes of a creator who already fits, marked by which search found each one. Every candidate also carries the **links pulled out of their bio**, typed and sorted by how much opening one will tell you — a repository to read the code in, their own site, a link hub that holds the real links — so vetting is reading the work rather than re-reading the follower count. Those links are never fetched here: they came out of a field the person being evaluated controls, so the host opens them, and the result says so. It does **not** measure audience overlap — that costs about nine credits a candidate, so the result says so and shows how to check a finalist rather than faking the signal. |
125
125
  | `find_people_with_problem` | 6 per platform | Find people **describing a problem in their own words** — prospects, early users, anyone whose complaint your product answers. Say the problem the way a person would say it and it searches for posts shaped like that complaint rather than for the topic it is about: searching the topic finds marketing about it, which is what `discover_social_posts` is for. Widens the query into the forms a complaint takes — the plain phrasing, the does-anyone-else question, the is-there-a-tool ask — and marks which one surfaced each post. A wide cheap net and **not** a filter: the posts come back for you to judge, and plenty will be off-target. Defaults to reddit, where people describe workflow pain in sentences. Each platform is searched once per query shape — three by default, at 2 credits a search. Not searchable here: linkedin, which has no keyword post search upstream. Pass `verbatim: true` to have every item rendered whole for quoting rather than clipped to fit; same price. |
126
126
  | `show_collab_shortlist` | free | Draws the candidates you scored, ranked, and asks the user which to approach. The scores are attributed to your model, not presented as a nooticr rating, and a candidate scored without anything having been opened is marked unverified. Makes no requests. |
127
- | `why_did_this_underperform` | 3 | One post against the creator's own recent distribution, with the post taken back out of its own baseline. Returns median, quartiles, ratio and percentile, so the answer can be "this is an ordinary result, not a failure". Different question from `compare_posts`, which weighs two URLs you already picked. |
127
+ | `why_did_this_underperform` | 3 | One post against the creator's own recent distribution, with the post taken back out of its own baseline. Returns median, quartiles, ratio and percentile, so the answer can be "this is an ordinary result, not a failure". Different question from `compare_social_posts`, which weighs two URLs you already picked. |
128
128
  | `what_should_i_make_next` | 2 + 2 per post read + 2 (12 by default) | Demand against supply: what your commenters explicitly ask for, set beside what a niche sweep shows is already being made. A gap nobody asked for is noise; a request nobody serves is the opportunity. Falls back to your most-used hashtag when you name no niche. |
129
129
  | `detect_spoken_mentions` | 2 | **Every brand named out loud in one video.** The spoken transcript and the written caption, side by side, for your model to list each brand with its verbatim sentence, the creator's framing and whether it was said, written or both. Works where the creator never enabled captions, because the audio is transcribed. 1 credit for `get_post_transcript` plus 1 for `get_social_media`; nothing when no transcript can be made. |
130
130
  | `search_spoken_mentions` | 2 per platform narrowed by niche + 2 per creator handle + 1 per transcript, up to `maxTranscripts` | **The mirror of `search_mentions`, for what was said rather than typed.** Narrows to candidate posts (a niche sweep, named handles, and/or your watchlist), transcribes only the most-viewed survivors up to a hard ceiling, and searches the words for the term — TikTok, YouTube and Douyin, and only where the creator actually enabled captions. Reports how many candidates were found, transcribed and matched, so the spend is legible. |
@@ -159,13 +159,13 @@ a `scanId` for the rest.
159
159
 
160
160
  | Tool | Credits | What it is for |
161
161
  |------|---------|----------------|
162
- | `write_hooks` | 2, or free | The source post and its transcript, to write openings against. Give a topic instead of a url and it fetches nothing and costs nothing. |
163
- | `show_hooks` | free | Draws the hooks your model wrote from `write_hooks` — each one with the device it uses and who it stops. Makes no requests; it only renders what you pass it. |
164
- | `score_draft` | free | **Your** draft back with the rubric to hold it to — hook, clarity, payoff, specificity and fit, each scored 1-10, plus the three fixes worth making and a rewritten opening. Fetches nothing: the text is already yours. The only tool that runs before the content exists. |
165
- | `repurpose_post` | 2 | The source post and its transcript, to rewrite for other surfaces — X thread, LinkedIn post, carousel slides, YouTube metadata, newsletter. |
166
- | `show_repurposed_post` | free | Draws the rewritten copy your model produced from `repurpose_post` — one entry per surface. Makes no requests; it only renders what you pass it. |
167
- | `create_variants` | 2 | The post that worked, with its transcript, to build variants from: hook, the angle that changes, ordered shot beats and a CTA. |
168
- | `show_variants` | free | Draws the variants your model wrote from `create_variants` — each one's hook, angle, beats and CTA. Makes no requests; it only renders what you pass it. |
162
+ | `generate_hook_ideas` | 2, or free | The source post and its transcript, to write openings against. Give a topic instead of a url and it fetches nothing and costs nothing. |
163
+ | `show_hooks` | free | Draws the hooks your model wrote from `generate_hook_ideas` — each one with the device it uses and who it stops. Makes no requests; it only renders what you pass it. |
164
+ | `evaluate_social_draft` | free | **Your** draft back with the rubric to hold it to — hook, clarity, payoff, specificity and fit, each scored 1-10, plus the three fixes worth making and a rewritten opening. Fetches nothing: the text is already yours. The only tool that runs before the content exists. |
165
+ | `adapt_post_for_platform` | 2 | The source post and its transcript, to rewrite for other surfaces — X thread, LinkedIn post, carousel slides, YouTube metadata, newsletter. |
166
+ | `show_repurposed_post` | free | Draws the rewritten copy your model produced from `adapt_post_for_platform` — one entry per surface. Makes no requests; it only renders what you pass it. |
167
+ | `generate_post_variants` | 2 | The post that worked, with its transcript, to build variants from: hook, the angle that changes, ordered shot beats and a CTA. |
168
+ | `show_variants` | free | Draws the variants your model wrote from `generate_post_variants` — each one's hook, angle, beats and CTA. Makes no requests; it only renders what you pass it. |
169
169
 
170
170
  ### Account
171
171
 
@@ -173,6 +173,8 @@ a `scanId` for the rest.
173
173
  |------|---------|----------------|
174
174
  | `nooticr_getting_started` | free | **Start here.** Where this account stands — balance, connected accounts, watchlist — and the next calls worth making, each with its price and an example. The server's `instructions` point every host at it. |
175
175
  | `check_nooticr_credits` | free | Balance and billing URL. |
176
+ | `nooticr_settings_read` | free | Read your default social platform and result count. |
177
+ | `nooticr_settings_update` | free | Save the default platform and result count used by supported searches when arguments are omitted. |
176
178
  | `list_tool_runs` | free | Your tool-call history: what ran, whether it worked, and the credits each call actually took (0 for a free, refunded, cached or replayed call). `scope: "workspace"` shows every member's runs to owners and admins. |
177
179
  | `get_tool_run` | free | One run from that history by id, with its full error. |
178
180
  | `get_usage_report` | free | The run ledger added up over a window: total calls, failures and credits actually taken, per tool, per day and the most recent failures. `scope: "workspace"` adds one row per seat, for the workspace's owners and admins. |
@@ -194,7 +196,7 @@ Everything above reads someone else's content. These read and generate for **you
194
196
  | `get_scheduled_posts` | free | Your own scheduled and draft posts in the content pipeline — title, status, scheduled time, approval status. What is queued to publish. |
195
197
  | `get_post_performance` | free | Your own already-published posts with their engagement counters — views, likes, comments, shares, platform, post date. The raw performance history, not an interpretation of it; pair with `growth_brief` for that. |
196
198
  | `get_video_stats` | free | Your own most recently synced video performance stats across every connected creator — views, likes, comments, shares, plus a running total. Reads the last sync; does not trigger a new one. |
197
- | `get_google_analytics` | free | Your product's GA4 sync **as of its last sync into nooticr, not live** — property, date range, rows synced. The metrics feed `get_post_performance` and `growth_brief`. |
199
+ | `get_google_analytics_data` | free | Your product's GA4 sync **as of its last sync into nooticr, not live** — property, date range, rows synced. The metrics feed `get_post_performance` and `growth_brief`. |
198
200
  | `get_search_console_data` | free | Search Console clicks, impressions and top queries, as of the last sync — whether people search for what your content is about. |
199
201
  | `get_posthog_analytics` | free | Your PostHog pageview trend, as of the last sync — line a post's date up against traffic to your site. |
200
202
  | `get_content_plan` | free | The saved weekly content plan for a product, if one has been generated. `plan: null` when none has. |
@@ -280,13 +282,13 @@ Twenty frames is about 2.4% of a million-token context.
280
282
  | Tool | What comes back | Credits |
281
283
  |---|---|---|
282
284
  | `analyze_post` | frames as images, plus the transcript and stats | **3** (2 + 1) |
283
- | `analyze_comments` | the comments, each with an id, and the labels to use | **2** |
285
+ | `summarize_post_comments` | the comments, each with an id, and the labels to use | **2** |
284
286
  | `analyze_creator_profile` | the creator's recent posts and their numbers | **2** |
285
- | `score_draft` | your own draft, and the rubric to score it against | **free** |
287
+ | `evaluate_social_draft` | your own draft, and the rubric to score it against | **free** |
286
288
 
287
289
  The prices above are derived from the fetches each tool makes rather than
288
290
  written down twice — see `EVIDENCE_PLANS` and `planCost` in
289
- `src/shared/evidence.ts`. `score_draft` has no plan at all: it reviews text you
291
+ `src/shared/evidence.ts`. `evaluate_social_draft` has no plan at all: it reviews text you
290
292
  already have, so there is nothing to fetch and nothing to charge for.
291
293
 
292
294
  `show_comment_review` closes the loop: hand back your classifications and it
@@ -107,7 +107,7 @@ export function normaliseProducts(raw) {
107
107
  return {
108
108
  // Addressable, so the model's analysis can point back at the exact
109
109
  // review it read a driver or a barrier out of. Same shape
110
- // analyze_comments mints for a comment.
110
+ // summarize_post_comments mints for a comment.
111
111
  id: String(row.id ?? `review:${id}:${i}`),
112
112
  asin: id,
113
113
  brand: String(p.brand ?? ""),
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * ## Why
5
5
  *
6
- * `analyze_comments` used to fetch a comment section, send it to Gemini, and
6
+ * `summarize_post_comments` used to fetch a comment section, send it to Gemini, and
7
7
  * return Gemini's opinion. That cost 6 credits, took a round trip to a third
8
8
  * party, and died when that third party declined — measured: `Gemini text 403
9
9
  * Forbidden` while `get_post_comments` returned the same post's comments fine.
@@ -139,7 +139,7 @@ export function classificationBlock() {
139
139
  *
140
140
  * The view has drawn per-item category chips, a counted filter row and the
141
141
  * click-to-filter behind them for as long as they have existed — but only
142
- * `analyze_comments` ever handed a host the taxonomy, so on a brand sweep
142
+ * `summarize_post_comments` ever handed a host the taxonomy, so on a brand sweep
143
143
  * across nine networks, the most expensive call in the product, all three were
144
144
  * inert every time (#79). Nothing needed building; the host was simply never
145
145
  * asked.
@@ -140,7 +140,7 @@ function postLine(row, b) {
140
140
  /**
141
141
  * One comment, with its id.
142
142
  *
143
- * The id is the load-bearing field: `analyze_comments` asks for a
143
+ * The id is the load-bearing field: `summarize_post_comments` asks for a
144
144
  * classification per comment and `show_comment_review` takes those ids back.
145
145
  * A digest that renders the text and drops the id breaks the next call.
146
146
  */
@@ -264,7 +264,7 @@ export const EVIDENCE_PLANS = {
264
264
  ownIt,
265
265
  ].join("\n"),
266
266
  },
267
- compare_posts: {
267
+ compare_social_posts: {
268
268
  via: "get_social_media",
269
269
  args: (a) => ({ url: String(a.urls?.[0] ?? "") }),
270
270
  guidance: (a) => [
@@ -356,7 +356,7 @@ export const EVIDENCE_PLANS = {
356
356
  ownIt,
357
357
  ].join("\n"),
358
358
  },
359
- write_hooks: {
359
+ generate_hook_ideas: {
360
360
  via: "get_social_media",
361
361
  args: (a) => ({ url: url(a) }),
362
362
  also: { via: "get_post_transcript", args: (a) => ({ url: url(a) }) },
@@ -385,7 +385,7 @@ export const EVIDENCE_PLANS = {
385
385
  ].join("\n");
386
386
  },
387
387
  },
388
- create_variants: {
388
+ generate_post_variants: {
389
389
  via: "get_social_media",
390
390
  args: (a) => ({ url: url(a) }),
391
391
  also: { via: "get_post_transcript", args: (a) => ({ url: url(a) }) },
@@ -407,7 +407,7 @@ export const EVIDENCE_PLANS = {
407
407
  ].join("\n");
408
408
  },
409
409
  },
410
- repurpose_post: {
410
+ adapt_post_for_platform: {
411
411
  via: "get_social_media",
412
412
  args: (a) => ({ url: url(a) }),
413
413
  also: { via: "get_post_transcript", args: (a) => ({ url: url(a) }) },
@@ -429,14 +429,14 @@ export const EVIDENCE_PLANS = {
429
429
  },
430
430
  };
431
431
  /**
432
- * `score_draft` has no plan, and deliberately so.
432
+ * `evaluate_social_draft` has no plan, and deliberately so.
433
433
  *
434
434
  * It reviews text the caller already supplied, so there is nothing to fetch:
435
435
  * a plan for it would be a paid call that handed back the caller's own input.
436
436
  * It stays on the tool list as a free tool instead — see `scoreDraftGuidance`,
437
437
  * which is the whole of what it now does.
438
438
  */
439
- export const FETCHES_NOTHING = ["score_draft"];
439
+ export const FETCHES_NOTHING = ["evaluate_social_draft"];
440
440
  /**
441
441
  * What the calling model is asked to produce for a draft.
442
442
  *
@@ -447,7 +447,7 @@ export const FETCHES_NOTHING = ["score_draft"];
447
447
  *
448
448
  * The tool could have been dropped instead. It is kept because a model reading
449
449
  * a tool list treats the list as the menu of what is worth doing — with no
450
- * `score_draft` on it, "check this before I film it" stops being a step
450
+ * `evaluate_social_draft` on it, "check this before I film it" stops being a step
451
451
  * anybody takes, and the `check_my_draft` prompt loses its first move. Naming
452
452
  * the axes also makes two runs comparable, which free-form prose never is.
453
453
  */
@@ -30,7 +30,7 @@ export const SERVER_INSTRUCTIONS = [
30
30
  "confirmation flow before proceeding.",
31
31
  "Not sure where to start? Call nooticr_getting_started: it is free, reads this account's state, and names " +
32
32
  "the next calls worth making with their prices. Good first calls: get_social_media on a post URL " +
33
- "(1 credit), discover_social_posts on a niche (2), analyze_comments on a post (2).",
33
+ "(1 credit), discover_social_posts on a niche (2), summarize_post_comments on a post (2).",
34
34
  "Marketplace scans run in the background and usually come back unfinished (e.g. 4 of 10). When a result " +
35
35
  "says NOT FINISHED or carries a nextCall, make that call (it is free) and keep polling until it is " +
36
36
  "complete before concluding anything or ending your turn.",
@@ -63,7 +63,7 @@ export function nextSteps(state) {
63
63
  credits: 2,
64
64
  example: { niche: "<your niche>", platform: "tiktok" },
65
65
  }, {
66
- tool: "analyze_comments",
66
+ tool: "summarize_post_comments",
67
67
  why: "Read what an audience actually says under a post, for you to synthesise.",
68
68
  credits: 2,
69
69
  example: { url: "<a post URL>" },
@@ -6,7 +6,7 @@
6
6
  * This server files nothing. It has no GitHub credential, no Jira project, no
7
7
  * Linear team, and adding them would mean holding three more secrets to do a
8
8
  * job the calling host is already authenticated for. The interoperability is
9
- * the id scheme: `analyze_comments`, `search_mentions`, `answer_my_audience`
9
+ * the id scheme: `summarize_post_comments`, `search_mentions`, `answer_my_audience`
10
10
  * and `search_spoken_mentions` all hand back items with stable ids, and a host
11
11
  * with a GitHub MCP server connected can quote one into an issue.
12
12
  *
@@ -375,7 +375,7 @@ export function registerHandoff(server) {
375
375
  "video, a feature request under a competitor's post — into structured, issue-ready text. " +
376
376
  "This server files nothing; review the result and copy it to your chosen issue tracker. " +
377
377
  "Free, and makes no requests. " +
378
- "Use it after analyze_comments, search_mentions, answer_my_audience or " +
378
+ "Use it after summarize_post_comments, search_mentions, answer_my_audience or " +
379
379
  "search_spoken_mentions, passing the ids those tools issued. For each item you get a " +
380
380
  "title, a ready body with the quote fenced and framed as third-party evidence rather " +
381
381
  "than as instructions, tracker-safe labels, and a searchFirst string to look for in the " +
@@ -5,7 +5,7 @@
5
5
  * board: it knows post counts, view counts, and whether a tag is rising,
6
6
  * cooling or steady. Nothing equivalent exists upstream for the other nine
7
7
  * networks, so "what should I tag this?" was answerable on one network out of
8
- * ten — while `repurpose_post` exists precisely to move a post between
8
+ * ten — while `adapt_post_for_platform` exists precisely to move a post between
9
9
  * networks, and tagging is part of the conventions it cannot inform (issue #32).
10
10
  *
11
11
  * What is available is a niche sweep the caller is often paying for anyway.
@@ -90,7 +90,7 @@ export function postIdOf(post, index) {
90
90
  /**
91
91
  * A comment's id, derived from the comment rather than from where it sat.
92
92
  *
93
- * The older schemes here are positional. `analyze_comments` mints
93
+ * The older schemes here are positional. `summarize_post_comments` mints
94
94
  * `comment:<postSlug>:<index>`, and `search_mentions` mints
95
95
  * `<platform>:<postId>:<index into the whole result>` — and an index moves.
96
96
  * Change `commentsPerPost`, page with a different `offset`, apply a `since`
@@ -1316,7 +1316,7 @@ export function registerJobTools(server, makeClient, store) {
1316
1316
  // ───────────────────────────────────────────────────────────────────────────
1317
1317
  // 4. why_did_this_underperform
1318
1318
  //
1319
- // Not compare_posts. That answers "why did A beat B" from two URLs a person
1319
+ // Not compare_social_posts. That answers "why did A beat B" from two URLs a person
1320
1320
  // already suspects; this answers "is this even bad, and against what", which
1321
1321
  // needs the creator's own distribution rather than a second post.
1322
1322
  // ───────────────────────────────────────────────────────────────────────────
@@ -1328,7 +1328,7 @@ export function registerJobTools(server, makeClient, store) {
1328
1328
  "own baseline, and returns where it actually sits in the distribution — median, " +
1329
1329
  "quartiles, ratio and percentile — so the answer can be 'this is a normal result, not a " +
1330
1330
  "failure'. Consumes 3 nooticr credits (1 for the post, 2 for the window). Use when you " +
1331
- "have one post and no comparison; compare_posts is for two URLs you already picked.",
1331
+ "have one post and no comparison; compare_social_posts is for two URLs you already picked.",
1332
1332
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1333
1333
  outputSchema: OUTPUT_SCHEMAS.why_did_this_underperform,
1334
1334
  inputSchema: z
@@ -67,6 +67,8 @@ const CREATOR_UNIT = {
67
67
  * generic grey box that said "Working".
68
68
  */
69
69
  export const LOADING_PLANS = {
70
+ nooticr_settings_read: { label: "Reading your research defaults", kind: "text", n: 0, steps: [], free: true },
71
+ nooticr_settings_update: { label: "Saving your research defaults", kind: "text", n: 0, steps: [], free: true },
70
72
  // ─── Read a post ───
71
73
  get_social_media: { label: "Fetching the post", kind: "post", n: 1, steps: [step("get_social_media")] },
72
74
  get_post_transcript: {
@@ -98,18 +100,18 @@ export const LOADING_PLANS = {
98
100
  n: 1,
99
101
  steps: evidenceSteps("analyze_post_fast"),
100
102
  },
101
- analyze_comments: {
103
+ summarize_post_comments: {
102
104
  label: "Reading the comment section",
103
105
  kind: "text",
104
106
  n: 1,
105
107
  // Not an evidence plan: it proxies one get_post_comments and synthesises.
106
108
  steps: [step("get_post_comments")],
107
109
  },
108
- compare_posts: {
110
+ compare_social_posts: {
109
111
  label: "Fetching the first post",
110
112
  kind: "strip",
111
113
  n: 2,
112
- steps: evidenceSteps("compare_posts"),
114
+ steps: evidenceSteps("compare_social_posts"),
113
115
  note: "1 credit more for every further post you fetch yourself.",
114
116
  },
115
117
  // ─── Research ───
@@ -154,21 +156,21 @@ export const LOADING_PLANS = {
154
156
  },
155
157
  niche_report: { label: "Reading the niche", kind: "text", n: 1, steps: evidenceSteps("niche_report") },
156
158
  // ─── Make something ───
157
- write_hooks: { label: "Reading the source post", kind: "text", n: 1, steps: evidenceSteps("write_hooks") },
158
- create_variants: {
159
+ generate_hook_ideas: { label: "Reading the source post", kind: "text", n: 1, steps: evidenceSteps("generate_hook_ideas") },
160
+ generate_post_variants: {
159
161
  label: "Reading the post that worked",
160
162
  kind: "text",
161
163
  n: 1,
162
- steps: evidenceSteps("create_variants"),
164
+ steps: evidenceSteps("generate_post_variants"),
163
165
  },
164
- repurpose_post: {
166
+ adapt_post_for_platform: {
165
167
  label: "Reading the source post",
166
168
  kind: "text",
167
169
  n: 1,
168
- steps: evidenceSteps("repurpose_post"),
170
+ steps: evidenceSteps("adapt_post_for_platform"),
169
171
  },
170
172
  // Fetches nothing: the draft is already the caller's.
171
- score_draft: { label: "Scoring the draft", kind: "text", n: 1, steps: [], free: true },
173
+ evaluate_social_draft: { label: "Scoring the draft", kind: "text", n: 1, steps: [], free: true },
172
174
  // ─── Brand monitoring ───
173
175
  search_mentions: {
174
176
  label: "Sweeping for mentions",
@@ -451,7 +453,7 @@ export const LOADING_PLANS = {
451
453
  get_scheduled_posts: { label: "Reading your pipeline", kind: "list", n: 5, steps: [], free: true },
452
454
  get_post_performance: { label: "Reading your numbers", kind: "list", n: 5, steps: [], free: true },
453
455
  get_video_stats: { label: "Reading the last sync", kind: "strip", n: 3, steps: [], free: true },
454
- get_google_analytics: { label: "Reading your last GA4 sync", kind: "text", n: 1, steps: [], free: true },
456
+ get_google_analytics_data: { label: "Reading your last GA4 sync", kind: "text", n: 1, steps: [], free: true },
455
457
  get_search_console_data: { label: "Reading your last Search Console sync", kind: "text", n: 1, steps: [], free: true },
456
458
  get_posthog_analytics: { label: "Reading your last PostHog sync", kind: "text", n: 1, steps: [], free: true },
457
459
  get_brand_playbook: { label: "Reading the playbook", kind: "text", n: 1, steps: [], free: true },
@@ -474,7 +474,7 @@ export const OUTPUT_SCHEMAS = {
474
474
  * left optional because the schema is passthrough and a caller that stored
475
475
  * one should not find the key gone.
476
476
  */
477
- analyze_comments: open({
477
+ summarize_post_comments: open({
478
478
  guidance,
479
479
  summary: scalar(),
480
480
  themes: anyList(),
@@ -572,7 +572,7 @@ export const OUTPUT_SCHEMAS = {
572
572
  // show_comment_review: they only draw what they're handed.
573
573
  show_compared_posts: open({
574
574
  verification: checkedAgainstSession,
575
- posts: listOf(post).describe("The 2-5 posts being compared, same shape as compare_posts returned."),
575
+ posts: listOf(post).describe("The 2-5 posts being compared, same shape as compare_social_posts returned."),
576
576
  comparison: open({
577
577
  winner: scalar().describe("1-indexed position of the post that won, matching the posts array."),
578
578
  winnerReason: scalar(),
@@ -901,7 +901,7 @@ export const OUTPUT_SCHEMAS = {
901
901
  post: open({}).nullish(),
902
902
  evidenceFrom: listOf(z.string()),
903
903
  }),
904
- compare_posts: open({
904
+ compare_social_posts: open({
905
905
  ...evidence,
906
906
  posts: listOf(post),
907
907
  failed: anyList(),
@@ -1033,7 +1033,7 @@ export const OUTPUT_SCHEMAS = {
1033
1033
  })).optional(),
1034
1034
  mcpCredits,
1035
1035
  }),
1036
- write_hooks: open({
1036
+ generate_hook_ideas: open({
1037
1037
  ...evidence,
1038
1038
  hooks: listOf(open({
1039
1039
  hook: scalar(),
@@ -1043,7 +1043,7 @@ export const OUTPUT_SCHEMAS = {
1043
1043
  sourceUrl: scalar(),
1044
1044
  mcpCredits,
1045
1045
  }),
1046
- create_variants: open({
1046
+ generate_post_variants: open({
1047
1047
  ...evidence,
1048
1048
  variants: listOf(open({
1049
1049
  hook: scalar(),
@@ -1061,13 +1061,13 @@ export const OUTPUT_SCHEMAS = {
1061
1061
  * the rubric and the reading is the caller's. Same rule as `provider` above:
1062
1062
  * documenting a field that never arrives is worse than omitting it.
1063
1063
  */
1064
- score_draft: open({
1064
+ evaluate_social_draft: open({
1065
1065
  guidance,
1066
1066
  draft: scalar(),
1067
1067
  platform: scalar(),
1068
1068
  mcpCredits,
1069
1069
  }),
1070
- repurpose_post: open({
1070
+ adapt_post_for_platform: open({
1071
1071
  ...evidence,
1072
1072
  repurposed: open({}).nullish().describe("One entry per target surface."),
1073
1073
  post: post.nullish(),
@@ -1390,7 +1390,7 @@ export const OUTPUT_SCHEMAS = {
1390
1390
  // The connector reads (#104). Each is the backend's connector_sync_cache
1391
1391
  // row as stored, plus `synced_at`, the hoisted appId/appName, and the
1392
1392
  // `connector` this surface adds so the view knows which table to draw.
1393
- get_google_analytics: open({
1393
+ get_google_analytics_data: open({
1394
1394
  connector: scalar(),
1395
1395
  propertyId: scalar(),
1396
1396
  rowsSynced: scalar(),
@@ -383,7 +383,7 @@ export function registerOwnAccountTools(server, makeClient) {
383
383
  return failed(`${name} failed`, err);
384
384
  }
385
385
  });
386
- connectorRead("get_google_analytics", "google_analytics", "Read Google Analytics Data", "Your own product's Google Analytics (GA4) sync, as of its last sync into nooticr — not a live " +
386
+ connectorRead("get_google_analytics_data", "google_analytics", "Read Google Analytics Data", "Your own product's Google Analytics (GA4) sync, as of its last sync into nooticr — not a live " +
387
387
  "GA4 call: the property, the date range synced and how many daily rows came in. The metrics " +
388
388
  "themselves (installs, opens, purchases, revenue) feed get_post_performance and growth_brief " +
389
389
  "rather than being repeated here. Returns a message rather than an error when nothing has " +
@@ -147,9 +147,9 @@ export function registerPrompts(server) {
147
147
  },
148
148
  }, ({ draft, platform }) => userMessage(`Review this draft before I film it${platform ? ` for ${platform}` : ""}:\n\n"""\n${draft}\n"""\n\n` +
149
149
  `Work in this order:\n` +
150
- `1. score_draft on it — free, and it returns the draft with the rubric to score it against: ` +
150
+ `1. evaluate_social_draft on it — free, and it returns the draft with the rubric to score it against: ` +
151
151
  `hook, clarity, payoff, specificity and fit, plus the fixes worth making.\n` +
152
- `2. write_hooks on the same topic for alternative openings to choose between.\n\n` +
152
+ `2. generate_hook_ideas on the same topic for alternative openings to choose between.\n\n` +
153
153
  `Then tell me plainly: is the hook doing work in the first two seconds, where does attention ` +
154
154
  `leak, and which of the alternative hooks you would actually use and why. If the draft is ` +
155
155
  `fine as it stands, say so rather than inventing changes.\n\n${COST_RULE}`));
@@ -170,7 +170,7 @@ export function registerPrompts(server) {
170
170
  ? `3. analyze_post — the visual pass, since the framing and editing are the point here.\n`
171
171
  : `3. Skip analyze_post unless the fast pass leaves the visuals genuinely unexplained. It ` +
172
172
  `fetches frames on top of the transcript, so it costs a credit more; say so before spending it.\n`) +
173
- `4. analyze_comments — what the audience actually took away, which is often not what the ` +
173
+ `4. summarize_post_comments — what the audience actually took away, which is often not what the ` +
174
174
  `creator intended.\n\n` +
175
175
  `Then give me: the hook and why it stops someone, the structure beat by beat, the CTA, and the ` +
176
176
  `gap (if any) between what the post says and what the comments show people heard.\n\n${COST_RULE}`));
@@ -181,7 +181,7 @@ export function registerPrompts(server) {
181
181
  urls: z.string().describe("2-5 post URLs, separated by spaces or commas."),
182
182
  },
183
183
  }, ({ urls }) => userMessage(`Compare these posts and explain the performance gap:\n${urls}\n\n` +
184
- `Start with compare_posts, then fetch the remaining URLs it names with get_social_media — it ` +
184
+ `Start with compare_social_posts, then fetch the remaining URLs it names with get_social_media — it ` +
185
185
  `pulls the first post and leaves the comparison, which is the part you should be doing.\n\n` +
186
186
  `Then tell me: which won, what actually differed (hook, format, length, caption, tags), what ` +
187
187
  `they share that I should keep, and one concrete experiment that would test your explanation. ` +
@@ -197,9 +197,9 @@ export function registerPrompts(server) {
197
197
  },
198
198
  }, ({ url, angle }) => userMessage(`This post worked: ${url}. Tell me what to film next.\n\n` +
199
199
  `Work in this order:\n` +
200
- `1. analyze_comments — the audience already said what they want more of, and the questions they ` +
200
+ `1. summarize_post_comments — the audience already said what they want more of, and the questions they ` +
201
201
  `asked are the cheapest content ideas available.\n` +
202
- `2. create_variants${angle ? ` with the angle "${angle}"` : ""} — same mechanism, different ` +
202
+ `2. generate_post_variants${angle ? ` with the angle "${angle}"` : ""} — same mechanism, different ` +
203
203
  `execution, with hooks and shot beats.\n\n` +
204
204
  `Then give me a shortlist I could film this week, each with its hook, what changes versus the ` +
205
205
  `original, and which comment or pattern it is answering. Order them by how much evidence there ` +
@@ -217,7 +217,7 @@ export function registerPrompts(server) {
217
217
  },
218
218
  }, ({ url, targets }) => userMessage(`Repurpose this post for other surfaces: ${url}\n` +
219
219
  (targets ? `Targets: ${targets}\n` : "") +
220
- `\nUse repurpose_post. If the post's argument depends on the exact wording, pull ` +
220
+ `\nUse adapt_post_for_platform. If the post's argument depends on the exact wording, pull ` +
221
221
  `get_post_transcript first so the rewrite carries the real lines rather than a summary of them.\n\n` +
222
222
  `Then give me each version ready to paste, and note for each one what you changed about the ` +
223
223
  `shape and why that surface rewards it.\n\n${COST_RULE}`));
@@ -0,0 +1,69 @@
1
+ export const SETTINGS_READ_TOOL = "nooticr_settings_read";
2
+ export const SETTINGS_UPDATE_TOOL = "nooticr_settings_update";
3
+ export const DEFAULT_SOCIAL_PLATFORMS = [
4
+ "tiktok",
5
+ "instagram",
6
+ "youtube",
7
+ "twitter",
8
+ "reddit",
9
+ "douyin",
10
+ "xiaohongshu",
11
+ "weibo",
12
+ "bilibili",
13
+ ];
14
+ export const DEFAULT_NOOTICR_SETTINGS = {
15
+ defaultSocialPlatform: "tiktok",
16
+ // Keep existing tool behaviour until the user chooses another count.
17
+ defaultResultCount: 12,
18
+ };
19
+ export class MemorySettingsStore {
20
+ values = new Map();
21
+ async get(owner) {
22
+ const value = this.values.get(owner);
23
+ return value ? { ...value } : undefined;
24
+ }
25
+ async put(owner, value) {
26
+ this.values.set(owner, { ...value });
27
+ }
28
+ }
29
+ /** Durable account-scoped preferences for the Cloudflare-hosted MCP server. */
30
+ export class KvSettingsStore {
31
+ kv;
32
+ constructor(kv) {
33
+ this.kv = kv;
34
+ }
35
+ key(owner) {
36
+ return `plugin-settings:${owner}`;
37
+ }
38
+ async get(owner) {
39
+ const raw = await this.kv.get(this.key(owner));
40
+ if (!raw)
41
+ return undefined;
42
+ try {
43
+ const parsed = JSON.parse(raw);
44
+ const platform = DEFAULT_SOCIAL_PLATFORMS.find((candidate) => candidate === parsed.defaultSocialPlatform);
45
+ const count = parsed.defaultResultCount;
46
+ if (!platform || !Number.isInteger(count) || count < 3 || count > 12)
47
+ return undefined;
48
+ return { defaultSocialPlatform: platform, defaultResultCount: count };
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ async put(owner, value) {
55
+ await this.kv.put(this.key(owner), JSON.stringify(value));
56
+ }
57
+ }
58
+ /** Stable owner keys prefer the authenticated Nooticr account ID. */
59
+ export async function settingsOwnerFromAuth(authInfo, resolveAccountId) {
60
+ const token = authInfo?.token ?? "";
61
+ const accountId = token && resolveAccountId ? await resolveAccountId(token) : undefined;
62
+ if (accountId)
63
+ return `account:${accountId}`;
64
+ if (!token)
65
+ return "local";
66
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(token));
67
+ const hex = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
68
+ return `credential:${hex}`;
69
+ }
@@ -5,7 +5,7 @@
5
5
  * chosen — including the ones that watch a video. POLL_TIMEOUT_MS is five
6
6
  * minutes, so analyze_post could hold a synchronous tools/call open for that
7
7
  * long and a host had no way to put it in the background. Measured against the
8
- * live server, discover_social_posts takes 10.3s and compare_posts 10.4s; the
8
+ * live server, discover_social_posts takes 10.3s and compare_social_posts 10.4s; the
9
9
  * multimodal tools wrapped here are the slow ones above them.
10
10
  *
11
11
  * The three tools that run longest now accept a task. What they do is