@nooticr/mcp 1.26.79 → 1.26.81

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.
@@ -675,7 +675,7 @@ export function registerJobTools(server, makeClient, store) {
675
675
  "Costs 2 nooticr credits for the post list plus 2 per post opened — 14 credits at the " +
676
676
  "default of 6 posts. Use when the job is to answer people; search_mentions is for what " +
677
677
  "strangers say elsewhere.",
678
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
678
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
679
679
  outputSchema: OUTPUT_SCHEMAS.answer_my_audience,
680
680
  inputSchema: z
681
681
  .object({
@@ -861,15 +861,14 @@ export function registerJobTools(server, makeClient, store) {
861
861
  server.registerTool("track_creator", {
862
862
  title: "Track Competitor",
863
863
  _meta: viewMeta("track_creator"),
864
- description: "Track a competitor, keep an eye on a rival, see how someone is doing lately, catch what " +
865
- "they shipped since you last looked — this is the tool for all of those. What a creator " +
866
- "shipped recently and which of it beat THEIR OWN baseline: fetches their recent posts " +
864
+ description: "What a creator shipped recently and which of it beat THEIR OWN baseline: fetches their recent posts " +
867
865
  "once and scores each against the median of that same window, because a raw view count " +
868
866
  "mostly measures follower count — outperformance against themselves is the signal. If " +
869
867
  "they are on your watchlist it also marks what is new since your last track_creator " +
870
868
  "call and moves that marker forward. " +
871
- "Consumes 2 nooticr credits — one post list, whatever the window size. Use for a rival " +
872
- "you follow; analyze_creator_profile is the full teardown of one you do not.",
869
+ "Consumes 2 nooticr credits — one post list, whatever the window size. Use to track a " +
870
+ "competitor, keep an eye on a rival, see how someone is doing lately, or catch what they " +
871
+ "shipped since you last looked; analyze_creator_profile is for a one-off profile teardown.",
873
872
  annotations: {
874
873
  readOnlyHint: false,
875
874
  destructiveHint: true,
@@ -1059,15 +1058,15 @@ export function registerJobTools(server, makeClient, store) {
1059
1058
  title: "Compare Creators",
1060
1059
  _meta: viewMeta("compare_creators"),
1061
1060
  description: "Two to five creators side by side, each scored against THEIR OWN recent median rather " +
1062
- "than against each other's raw numbers — which is the only way the comparison means " +
1063
- "anything, because a raw view count mostly measures follower count. Returns per creator: " +
1061
+ "than against each other's raw numbers, because a raw view count mostly measures follower " +
1062
+ "count. Returns per creator: " +
1064
1063
  "how many posts were scored, their own median, the share of the window that beat it, how " +
1065
- "hard they beat it when they did, every post's ratio, and their best and worst post. The " +
1064
+ "hard they beat it when they did, every post's ratio, and their highest and lowest post. The " +
1066
1065
  "ranking is yours to make: 'how often' and 'how big' usually disagree, and a hit rate " +
1067
1066
  "over a short window is one post either way, so the window size travels with every " +
1068
1067
  "number. Fetches one post list per creator: 2 nooticr credits each, 4-10 in total. " +
1069
- "Use for 'is their hit rate better than mine'; track_creator is one creator alone.",
1070
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1068
+ "Use to compare hit rates between two to five creators, yourself included; track_creator covers one creator.",
1069
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1071
1070
  outputSchema: OUTPUT_SCHEMAS.compare_creators,
1072
1071
  inputSchema: z
1073
1072
  .object({
@@ -1131,13 +1130,13 @@ export function registerJobTools(server, makeClient, store) {
1131
1130
  title: "Watchlist Standings",
1132
1131
  _meta: viewMeta("watchlist_standings"),
1133
1132
  description: "The same comparison across everyone on your watchlist, answering 'who moved' in one " +
1134
- "call instead of one per creator. Each is scored against their own recent median, so the " +
1133
+ "call. Each is scored against their own recent median, so the " +
1135
1134
  "numbers are comparable between accounts of different sizes; the window size travels " +
1136
1135
  "with each so a short one is not ranked as if it were a season. Reads the watchlist " +
1137
1136
  "itself for free and fetches one post list per creator on it: 2 nooticr credits each, so " +
1138
1137
  "the price is the size of your watchlist — it confirms before spending. Use for the " +
1139
- "weekly 'who is accelerating'; catch_up_watchlist is what is NEW rather than how it did.",
1140
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1138
+ "weekly 'who is accelerating'; catch_up_watchlist lists what is new since the last check.",
1139
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1141
1140
  outputSchema: OUTPUT_SCHEMAS.watchlist_standings,
1142
1141
  inputSchema: z
1143
1142
  .object({
@@ -1215,7 +1214,7 @@ export function registerJobTools(server, makeClient, store) {
1215
1214
  "per candidate, so the result says so and tells you how to check a finalist yourself. " +
1216
1215
  "Consumes 2 nooticr credits, or 4 with a seed creator. Use to build a list to vet; " +
1217
1216
  "get_similar_creators is the raw lookalike call.",
1218
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1217
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1219
1218
  outputSchema: OUTPUT_SCHEMAS.who_should_i_work_with,
1220
1219
  inputSchema: z
1221
1220
  .object({
@@ -1364,7 +1363,7 @@ export function registerJobTools(server, makeClient, store) {
1364
1363
  "quartiles, ratio and percentile — so the answer can be 'this is a normal result, not a " +
1365
1364
  "failure'. Consumes 3 nooticr credits (1 for the post, 2 for the window). Use when you " +
1366
1365
  "have one post and no comparison; compare_social_posts is for two URLs you already picked.",
1367
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1366
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1368
1367
  outputSchema: OUTPUT_SCHEMAS.why_did_this_underperform,
1369
1368
  inputSchema: z
1370
1369
  .object({
@@ -1485,7 +1484,7 @@ export function registerJobTools(server, makeClient, store) {
1485
1484
  "Consumes 2 nooticr credits for the post list, 2 per post read for comments, and 2 for the " +
1486
1485
  "niche sweep — 12 at the default of 4 posts. Use to decide what to film; niche_report " +
1487
1486
  "covers the supply half alone.",
1488
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1487
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1489
1488
  outputSchema: OUTPUT_SCHEMAS.what_should_i_make_next,
1490
1489
  inputSchema: z
1491
1490
  .object({
@@ -1669,8 +1668,7 @@ export function registerJobTools(server, makeClient, store) {
1669
1668
  "anything: each row is a draft, and a reply is posted only after the creator confirms its " +
1670
1669
  "exact text. Each comment shows with what you decided to do " +
1671
1670
  "about it and your draft underneath, grouped under its post, so they can work through " +
1672
- "them one at a time and skip the rest. Call this after you have drafted the replies, not " +
1673
- "instead of drafting them.",
1671
+ "them one at a time and skip the rest. Call this after you have drafted the replies.",
1674
1672
  annotations: {
1675
1673
  readOnlyHint: true,
1676
1674
  destructiveHint: false,
@@ -1810,14 +1808,14 @@ export function registerJobTools(server, makeClient, store) {
1810
1808
  title: "Search Spoken Mentions",
1811
1809
  _meta: viewMeta("search_spoken_mentions"),
1812
1810
  description: "Brand mentions people SAY but never type. search_mentions reads captions, post bodies " +
1813
- "and comments — text — so a video that names a brand only out loud is invisible to it. " +
1811
+ "and comments — text — so it does not see a brand named only out loud in a video. " +
1814
1812
  "This reads the words actually spoken, from the platform's own caption track (not our own " +
1815
1813
  "speech recognition — see get_post_transcript), and searches them for a term. Narrows to " +
1816
1814
  "candidate posts first (a niche/keyword sweep, named creator handles, and/or your " +
1817
1815
  "watchlist), transcribes only the most-viewed survivors up to a hard ceiling you set, and " +
1818
1816
  "returns the matched line with surrounding context so tone can be judged. TikTok, YouTube " +
1819
- "and Douyin — the networks whose post detail already carries a caption track, which is " +
1820
- "what makes a transcript one cheap read rather than a wait. Every other network's audio " +
1817
+ "and Douyin — the networks whose post detail already carries a caption track, so a " +
1818
+ "transcript is read directly rather than waited for. Every other network's audio " +
1821
1819
  "can be transcribed too, at the same price, but not yet from here: see " +
1822
1820
  "get_post_transcript for that route. Even on these three a video whose creator disabled " +
1823
1821
  "captions is invisible to this tool; coverage is real but " +
@@ -1828,7 +1826,7 @@ export function registerJobTools(server, makeClient, store) {
1828
1826
  "count. Above 6 credits worst-case it asks first, like search_mentions. Use when a term " +
1829
1827
  "might be spoken on camera but not written anywhere; search_mentions is for what people " +
1830
1828
  "typed.",
1831
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
1829
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
1832
1830
  outputSchema: OUTPUT_SCHEMAS.search_spoken_mentions,
1833
1831
  inputSchema: z
1834
1832
  .object({
@@ -2130,14 +2128,14 @@ export function registerJobTools(server, makeClient, store) {
2130
2128
  description: "Find people describing a problem in their own words — prospects, early users, anyone " +
2131
2129
  "whose complaint your product answers. Say the problem the way a person would say it " +
2132
2130
  "(\"I spend hours checking competitors by hand\") and this searches for posts shaped like " +
2133
- "that complaint, not for the topic it is about. That difference is the point: searching " +
2134
- "the TOPIC finds marketing about it, which is what discover_social_posts is for. " +
2131
+ "that complaint, not for the topic it is about. Searching the TOPIC returns marketing " +
2132
+ "about it; discover_social_posts covers that. " +
2135
2133
  "Widens the query into the forms a complaint takes — the plain phrasing, the " +
2136
2134
  "does-anyone-else question, the is-there-a-tool ask — and merges what each returns. " +
2137
- "It is a wide cheap net and NOT a filter: the posts come back for you to judge, and " +
2135
+ "It is a broad search and NOT a filter: the posts come back for you to judge, and " +
2138
2136
  "many will be off-target. Consumes 2 nooticr credits per search, and each platform is searched once per query shape — three by default, so 6 credits per platform. Defaults to " +
2139
- "reddit, where people describe workflow pain in sentences; add twitter for volume. Not searchable here: linkedin.",
2140
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2137
+ "reddit, where people describe workflow pain in sentences; add twitter for volume. Not searchable here: linkedin. Use to find prospects or early users by the way they describe a problem.",
2138
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2141
2139
  outputSchema: OUTPUT_SCHEMAS.find_people_with_problem,
2142
2140
  inputSchema: z
2143
2141
  .object({
@@ -503,6 +503,11 @@ export const LOADING_PLANS = {
503
503
  list_employees: { label: "Reading your employees", kind: "list", n: 3, steps: [], free: true },
504
504
  pause_employee: { label: "Updating the employee", kind: "text", n: 1, steps: [], free: true },
505
505
  fire_employee: { label: "Letting the employee go", kind: "text", n: 1, steps: [], free: true },
506
+ // Creator CRM (crm.ts): free, and none of it fetches.
507
+ crm_upsert_creator: { label: "Saving the creator", kind: "text", n: 1, steps: [], free: true },
508
+ crm_list_creators: { label: "Reading your creator pipeline", kind: "list", n: 3, steps: [], free: true },
509
+ crm_update_status: { label: "Updating the creator's status", kind: "text", n: 1, steps: [], free: true },
510
+ crm_log_interaction: { label: "Logging the interaction", kind: "text", n: 1, steps: [], free: true },
506
511
  list_pending_actions: { label: "Reading what awaits approval", kind: "list", n: 3, steps: [], free: true },
507
512
  get_employee_audit: { label: "Reading the audit trail", kind: "list", n: 3, steps: [], free: true },
508
513
  approve_action: { label: "Approving the action", kind: "text", n: 1, steps: [], free: true },
@@ -274,7 +274,7 @@ export function registerMarketplaceTools(server, makeClient) {
274
274
  "repeating until the scan is done. Never conclude, or end your turn, on an incomplete " +
275
275
  "scan: a read of 4 of 10 listings presented as the category is wrong. " +
276
276
  "For Amazon specifically, scan_amazon_category returns the same shape plus Amazon's own " +
277
- "review-aspect counts.",
277
+ "review-aspect counts. Use for a category or a competitive set on one of these marketplaces; get_marketplace_product reads one product.",
278
278
  annotations: {
279
279
  readOnlyHint: false,
280
280
  destructiveHint: false,
@@ -407,7 +407,7 @@ export function registerMarketplaceTools(server, makeClient) {
407
407
  "of the same marketplace and `query`, so it fetches nothing and never re-scans — run " +
408
408
  "scan_marketplace_category first, and again later, to have two runs to compare. `new` and " +
409
409
  "`gone` mean entered or left the results, not listed or delisted. A category scanned only " +
410
- "once has no earlier run and says so. Free.",
410
+ "once has no earlier run and says so. Free. Use when the user asks what changed in a category since an earlier scan.",
411
411
  annotations: {
412
412
  readOnlyHint: true,
413
413
  destructiveHint: false,
@@ -442,7 +442,7 @@ export function registerMarketplaceTools(server, makeClient) {
442
442
  server.registerTool("get_price_history", {
443
443
  title: "Get Price History",
444
444
  description: "One product's price over time — or its rating, review count or availability with `metric` — " +
445
- "from the observations stored each time the product was scanned. `marketplace` and `id` name " +
445
+ "from the observations stored each time the product was scanned. Use when someone asks how a product's price, rating, review count or stock moved over time. `marketplace` and `id` name " +
446
446
  "the product the same way get_marketplace_product does; `from` and `to` take dates (default: " +
447
447
  "the last 30 days). Returns the points oldest first with min, max, first, last and the " +
448
448
  "percentage change from first to last. A point is stored only when something changed, so the " +
@@ -498,7 +498,7 @@ export function registerMarketplaceTools(server, makeClient) {
498
498
  annotations: {
499
499
  readOnlyHint: true,
500
500
  destructiveHint: false,
501
- idempotentHint: true,
501
+ idempotentHint: false,
502
502
  openWorldHint: true,
503
503
  },
504
504
  outputSchema: OUTPUT_SCHEMAS.get_marketplace_product,
@@ -549,7 +549,7 @@ export function registerMarketplaceTools(server, makeClient) {
549
549
  "cite that the scan does not contain is flagged. Without a scanId, listings you pass are " +
550
550
  "drawn marked as not checked against a scan. Free. Your read is attributed to you, never " +
551
551
  "presented as a nooticr rating of anyone's product. For Amazon scans use " +
552
- "show_amazon_category_insights. Call this after you have read the reviews, not instead of reading them.",
552
+ "show_amazon_category_insights. Call this after you have read the reviews and written the read.",
553
553
  annotations: {
554
554
  readOnlyHint: true,
555
555
  destructiveHint: false,
@@ -1785,6 +1785,11 @@ export const OUTPUT_SCHEMAS = {
1785
1785
  action: anyObject().nullish(),
1786
1786
  }),
1787
1787
  reject_action: open({ rejected: scalar(), action: anyObject().nullish() }),
1788
+ // Creator CRM (nooticr-server E1): the creator/interaction views are the server's own shapes.
1789
+ crm_upsert_creator: open({ creator: anyObject().nullish() }),
1790
+ crm_list_creators: open({ creators: listOf(open({ id: scalar(), platform: scalar(), handle: scalar(), status: scalar() })) }),
1791
+ crm_update_status: open({ creator: anyObject().nullish() }),
1792
+ crm_log_interaction: open({ interaction: anyObject().nullish() }),
1788
1793
  list_employees: open({ employees: listOf(open({ id: scalar(), name: scalar(), status: scalar() })) }),
1789
1794
  list_pending_actions: open({ actions: listOf(open({ id: scalar(), employeeId: scalar() })) }),
1790
1795
  // AU1 (nooticr-server employees/mcp.rs get_employee_audit; event shape from employee_audit_view::event_view).
@@ -154,7 +154,7 @@ export function registerOwnAccountTools(server, makeClient) {
154
154
  description: "Patch your own product's fields — omitted arguments leave their column unchanged. Takes " +
155
155
  "appId (optional when your workspace has only one product); every other field is " +
156
156
  "snake_case, the same names create_product takes and for the same reason — the backend " +
157
- "reads them by exact key. Free — no AI call, just a row.",
157
+ "reads them by exact key. Free — no AI call, just a row. Use when the user wants to change details of a product that already exists.",
158
158
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
159
159
  inputSchema: z
160
160
  .object({
@@ -191,7 +191,7 @@ export function registerOwnAccountTools(server, makeClient) {
191
191
  title: "Get Scheduled Posts",
192
192
  description: "Your own scheduled and draft posts in the content pipeline — title, status, scheduled " +
193
193
  "time, approval status. What is queued to publish, not what already has (see " +
194
- "get_post_performance for that). Does not publish or change anything. No cost to call.",
194
+ "get_post_performance for that). Does not publish or change anything. No cost to call. Use when the user asks what is queued or drafted in their content pipeline.",
195
195
  _meta: viewMeta("get_scheduled_posts"),
196
196
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
197
197
  inputSchema: z
@@ -218,7 +218,7 @@ export function registerOwnAccountTools(server, makeClient) {
218
218
  title: "Get Post Performance",
219
219
  description: "Your own already-published posts with their engagement counters — views, likes, " +
220
220
  "comments, shares, platform, post date. This is the raw performance history, not an " +
221
- "interpretation of it; pair with growth_brief for that. No cost to call.",
221
+ "interpretation of it; pair with growth_brief for that. No cost to call. Use when the user asks how their own published posts performed.",
222
222
  _meta: viewMeta("get_post_performance"),
223
223
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
224
224
  inputSchema: z
@@ -245,7 +245,7 @@ export function registerOwnAccountTools(server, makeClient) {
245
245
  title: "Get Video Stats",
246
246
  description: "Your own most recently synced video performance stats across every connected creator — " +
247
247
  "views, likes, comments, shares, plus a running total. Reads the last sync; does not " +
248
- "trigger a new one. No cost to call.",
248
+ "trigger a new one. No cost to call. Use when the user asks for their own synced video numbers.",
249
249
  _meta: viewMeta("get_video_stats"),
250
250
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
251
251
  inputSchema: z
@@ -285,7 +285,7 @@ export function registerOwnAccountTools(server, makeClient) {
285
285
  title: "Get Content Plan",
286
286
  description: "The saved weekly content plan for your own product, if one has been generated (see " +
287
287
  "generate_content_plan). Read-only and free even when a plan exists — generating a new " +
288
- "one is the paid step. Returns plan: null when none has been made yet. No cost to call.",
288
+ "one is the paid step. Returns plan: null when none has been made yet. No cost to call. Use when the user asks to see the content plan already generated for their product.",
289
289
  _meta: viewMeta("get_content_plan"),
290
290
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
291
291
  inputSchema: z
@@ -316,7 +316,7 @@ export function registerOwnAccountTools(server, makeClient) {
316
316
  description: "Your own product's brand playbook — name, description and the playbook text — if one has " +
317
317
  "been configured, in the dashboard or by a previous analyze_product run. Read-only: never " +
318
318
  "creates or edits a playbook itself. Returns available: false when none exists yet. Takes " +
319
- "appId (optional when your workspace has only one product). No cost to call.",
319
+ "appId (optional when your workspace has only one product). No cost to call. Use when a task needs the product's stored brand voice or positioning.",
320
320
  _meta: viewMeta("get_brand_playbook"),
321
321
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
322
322
  inputSchema: z
@@ -388,7 +388,8 @@ export function registerOwnAccountTools(server, makeClient) {
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 " +
390
390
  "ever been synced; connect GA4 in API Connections first. Takes appId (optional when your " +
391
- "workspace has only one product). No cost to call.");
391
+ "workspace has only one product). No cost to call. Use to check whether GA4 data is synced " +
392
+ "and for which dates.");
392
393
  connectorRead("get_search_console_data", "search_console", "Get Search Console Data", "Your own product's Google Search Console data — total clicks and impressions and the top " +
393
394
  "search queries — as of its last sync into nooticr, not a live Search Console call. Use it " +
394
395
  "to see whether people search for what your social content is about. Returns a message " +
@@ -411,7 +412,7 @@ export function registerOwnAccountTools(server, makeClient) {
411
412
  "background: returns a jobId immediately rather than the finished analysis — poll it with " +
412
413
  "analyze_product_status. Billed like the dashboard's own analyze job: 10 of your " +
413
414
  "workspace's plan AI credits (first analysis free per workspace), a different balance from " +
414
- "your personal MCP credits and not tracked by check_nooticr_credits.",
415
+ "your personal MCP credits and not tracked by check_nooticr_credits. Use when the user wants a brand playbook generated or refreshed for their product.",
415
416
  // Not view-less for lack of trying: the immediate reply is only a
416
417
  // jobId and state: "pending", nothing to draw yet. analyze_product_status,
417
418
  // which returns the finished playbook, is where the view belongs — see
@@ -440,7 +441,7 @@ export function registerOwnAccountTools(server, makeClient) {
440
441
  title: "Analyze Product Status",
441
442
  description: "Poll a job started by analyze_product. Takes jobId. Returns state (pending, thinking, " +
442
443
  "done, error) and, once done, the generated analysis/brand playbook. Free to poll — the " +
443
- "cost was already charged when analyze_product started the job.",
444
+ "cost was already charged when analyze_product started the job. Use after analyze_product, until the job reports done or error.",
444
445
  _meta: viewMeta("analyze_product_status"),
445
446
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
446
447
  inputSchema: z
@@ -467,13 +468,13 @@ export function registerOwnAccountTools(server, makeClient) {
467
468
  "appId plus the draft fields to review something that only exists as arguments. Nothing is " +
468
469
  "published. If the AI reviewer is unavailable the result carries degraded: true and a " +
469
470
  "warning — treat those scores as generic placeholders, never as real feedback. Free — " +
470
- "nothing is billed for this, same as the dashboard's own pre-publish review.",
471
+ "nothing is billed for this, same as the dashboard's own pre-publish review. Use when the user wants feedback on a post before publishing it.",
471
472
  _meta: viewMeta("review_post"),
472
473
  // Not read-only: given a postId the backend saves the review and score
473
474
  // onto that scheduled post (`save_ai_review`), overwriting whatever was
474
475
  // there. Free, which is not the same thing — a host that auto-approves
475
476
  // read-only tools would let this rewrite pipeline data unattended.
476
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
477
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
477
478
  inputSchema: z
478
479
  .object({
479
480
  postId: z.number().int().optional().describe("A post already in your pipeline. Alternative to appId + draft fields."),
@@ -505,9 +506,9 @@ export function registerOwnAccountTools(server, makeClient) {
505
506
  "schedule anything — this only returns the draft text for you to present, refine, or hand " +
506
507
  "to a tool that persists it. Billed like the dashboard's own Draft Post button: consumes " +
507
508
  "your workspace's plan AI credits, a different balance from your personal MCP credits and " +
508
- "not tracked by check_nooticr_credits.",
509
+ "not tracked by check_nooticr_credits. Use when the user asks for a post written for their own product.",
509
510
  _meta: viewMeta("draft_post"),
510
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
511
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
511
512
  inputSchema: z
512
513
  .object({
513
514
  appId: z
@@ -537,13 +538,13 @@ export function registerOwnAccountTools(server, makeClient) {
537
538
  "plus whatever analytics (GA4, Search Console, PostHog) and fleet analysis you have " +
538
539
  "synced. It changes nothing of yours, but it is not free: billed like the dashboard's own " +
539
540
  "Growth Brief button, from your workspace's plan AI credits rather than your personal " +
540
- "MCP credits.",
541
+ "MCP credits. Use when the user asks how their product's social presence is doing and what to do next.",
541
542
  _meta: viewMeta("growth_brief"),
542
543
  // Spending the workspace's plan AI credits (`CREDIT_GROWTH_BRIEF`) is
543
544
  // what puts this outside read-only, by the same rule that already
544
545
  // covers draft_post and generate_content_plan. It writes no content of
545
546
  // the user's, which is why the description says so in those words.
546
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
547
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
547
548
  inputSchema: z
548
549
  .object({
549
550
  appId: z
@@ -569,7 +570,7 @@ export function registerOwnAccountTools(server, makeClient) {
569
570
  "a hook, caption, hashtags and a full production script for each, grounded in what already " +
570
571
  "worked in your post history. Saves the generated plan (fetch it later with the free " +
571
572
  "get_content_plan) but does not schedule or publish any post. Billed like the dashboard's " +
572
- "own Content Plan button: your workspace's plan AI credits, not your personal MCP credits.",
573
+ "own Content Plan button: your workspace's plan AI credits, not your personal MCP credits. Use when the user asks for a week of planned posts for their product.",
573
574
  _meta: viewMeta("generate_content_plan"),
574
575
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
575
576
  inputSchema: z
@@ -601,11 +602,11 @@ export function registerOwnAccountTools(server, makeClient) {
601
602
  description: "Generate timed on-screen caption cues for a video on your own product — a transcript " +
602
603
  "plus start/end-timed lines. Does not burn captions onto any video or touch stored media; " +
603
604
  "only returns the cue data. If no AI provider is configured the result carries " +
604
- "provider: \"mock\" placeholder captions instead of failing — check that field before " +
605
+ "provider: \"mock\" placeholder captions rather than an error — check that field before " +
605
606
  "treating the captions as real. Billed like the dashboard's own Generate Captions button: " +
606
- "your workspace's plan AI credits, not your personal MCP credits.",
607
+ "your workspace's plan AI credits, not your personal MCP credits. Use when the user needs timed on-screen captions for one of their videos.",
607
608
  _meta: viewMeta("generate_captions"),
608
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
609
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: false },
609
610
  inputSchema: z
610
611
  .object({
611
612
  appId: z
@@ -35,7 +35,7 @@ export function registerReplyTools(server, makeClient) {
35
35
  "connected with capabilities: [\"manage_comments\"] (see connect_social_account); networks " +
36
36
  "that cannot be answered come back under `unsupported` with the reason. Optional platform, " +
37
37
  "influencerId, postId, sinceDays (1-30, default 7), limit (1-100, default 30), " +
38
- "includeAnswered. Read-only: it sends nothing. No cost to call.",
38
+ "includeAnswered. Read-only: it sends nothing. No cost to call. Use when the user wants to reply to comments on their own connected account.",
39
39
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
40
40
  outputSchema: OUTPUT_SCHEMAS.list_comments_to_answer,
41
41
  inputSchema: z
@@ -64,7 +64,7 @@ export function registerReplyTools(server, makeClient) {
64
64
  "posted. Pass `text` to use the user's own wording (no cost), or omit it and nooticr " +
65
65
  "writes one (costs 1 nooticr credit); `guidance` steers the writing. Replies may not contain links, " +
66
66
  "emails, phone numbers or @-mentions of anyone but the commenter. Returns a draftId and a " +
67
- "confirmationToken: show the reply to the user first, then post it with post_comment_reply.",
67
+ "confirmationToken: show the reply to the user first, then post it with post_comment_reply. Use when the user wants to answer one comment from list_comments_to_answer.",
68
68
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
69
69
  outputSchema: OUTPUT_SCHEMAS.draft_comment_reply,
70
70
  inputSchema: z
@@ -93,7 +93,7 @@ export function registerReplyTools(server, makeClient) {
93
93
  "has seen and agreed to the exact text: pass the draftId, confirm: true and the " +
94
94
  "confirmationToken from draft_comment_reply. Each comment is answered at most once and " +
95
95
  "daily limits apply. It writes to a third-party network and cannot be undone from here. " +
96
- "No cost to call.",
96
+ "No cost to call. Use only once the user has approved a draft's exact text.",
97
97
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
98
98
  outputSchema: OUTPUT_SCHEMAS.post_comment_reply,
99
99
  inputSchema: z
@@ -29,6 +29,8 @@ const cell = z
29
29
  const rows = z.array(z.array(cell));
30
30
  const READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
31
31
  const WRITE = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
32
+ // Overwriting a range replaces whatever the cells held, which append and add-sheet never do.
33
+ const OVERWRITE = { ...WRITE, destructiveHint: true };
32
34
  export function registerSheetsTools(server, makeClient) {
33
35
  const passthrough = (name) => async (args, extra) => {
34
36
  const client = await makeClient({ ...extra, arguments: args });
@@ -43,7 +45,7 @@ export function registerSheetsTools(server, makeClient) {
43
45
  title: "Create Spreadsheet",
44
46
  description: "Create a Google spreadsheet in the connected Google account, with the named sheets and a header row on each. " +
45
47
  "Returns its id and URL; Nooticr owns the file, so later writes need no approval. " +
46
- "Requires the product's Google Sheets connection. Free.",
48
+ "Requires the product's Google Sheets connection. Free. Use when the user wants results written to a new Google spreadsheet.",
47
49
  annotations: WRITE,
48
50
  inputSchema: z
49
51
  .object({
@@ -60,7 +62,7 @@ export function registerSheetsTools(server, makeClient) {
60
62
  server.registerTool("sheets_list", {
61
63
  title: "List Spreadsheets",
62
64
  description: "List the spreadsheets this connection can see: the ones Nooticr created and the ones the customer picked. " +
63
- "Each says whether it was created or picked and whether the employee may edit it without approval. Free.",
65
+ "Each says whether it was created or picked and whether the employee may edit it without approval. Free. Use to find a spreadsheet's id before reading or writing it.",
64
66
  annotations: READ,
65
67
  inputSchema: z.object({ appId, limit: z.number().int().min(1).max(100).optional() }).strict(),
66
68
  outputSchema: OUTPUT_SCHEMAS.sheets_list,
@@ -72,7 +74,7 @@ export function registerSheetsTools(server, makeClient) {
72
74
  'and a range open at the bottom ("A:F", "Ledger!$A:$F") or longer than the cap is cut to the rows that fit, ' +
73
75
  "with truncated=true when more may remain: read further down with a range that starts after the last row. " +
74
76
  'A bare name without quotes ("Expenses") is passed to Google as written, since it may be a named range, ' +
75
- 'so name a tab as "Ledger!A:F" or "\'My Tab\'" to have it bounded. Free.',
77
+ 'so name a tab as "Ledger!A:F" or "\'My Tab\'" to have it bounded. Free. Use when the user asks about data held in one of their connected spreadsheets.',
76
78
  annotations: READ,
77
79
  inputSchema: z.object({ appId, id: spreadsheetId, range: z.string() }).strict(),
78
80
  outputSchema: OUTPUT_SCHEMAS.sheets_read,
@@ -81,7 +83,7 @@ export function registerSheetsTools(server, makeClient) {
81
83
  title: "Append Spreadsheet Rows",
82
84
  description: "Append rows after the last row of a sheet (tab). Values are typed: numbers stay numbers and ISO dates become sheet dates. " +
83
85
  "At most 10000 cells per call. Free on a file Nooticr created or one the customer allowed you to edit; " +
84
- "on any other file the customer picked it waits for their approval.",
86
+ "on any other file the customer picked it waits for their approval. Use to add new rows below the existing data in a sheet.",
85
87
  annotations: WRITE,
86
88
  inputSchema: z.object({ appId, id: spreadsheetId, sheet: z.string().describe("Tab name."), rows }).strict(),
87
89
  outputSchema: OUTPUT_SCHEMAS.sheets_append_rows,
@@ -89,14 +91,14 @@ export function registerSheetsTools(server, makeClient) {
89
91
  server.registerTool("sheets_update_range", {
90
92
  title: "Update Spreadsheet Range",
91
93
  description: 'Overwrite a range (A1 notation, e.g. "Ledger!B2:C3") with values. Typed like sheets_append_rows; ' +
92
- "at most 10000 cells per call. Same approval rule as sheets_append_rows: free on a file Nooticr created or one the customer allowed you to edit, otherwise it waits for their approval.",
93
- annotations: WRITE,
94
+ "at most 10000 cells per call. Same approval rule as sheets_append_rows: free on a file Nooticr created or one the customer allowed you to edit, otherwise it waits for their approval. Use to change values that are already in a sheet.",
95
+ annotations: OVERWRITE,
94
96
  inputSchema: z.object({ appId, id: spreadsheetId, range: z.string(), values: rows }).strict(),
95
97
  outputSchema: OUTPUT_SCHEMAS.sheets_update_range,
96
98
  }, passthrough("sheets_update_range"));
97
99
  server.registerTool("sheets_add_sheet", {
98
100
  title: "Add Spreadsheet Tab",
99
- description: "Add an empty sheet (tab) to a spreadsheet. Same approval rule as sheets_append_rows: free on a file Nooticr created or one the customer allowed you to edit, otherwise it waits for their approval.",
101
+ description: "Add an empty sheet (tab) to a spreadsheet. Same approval rule as sheets_append_rows: free on a file Nooticr created or one the customer allowed you to edit, otherwise it waits for their approval. Use when a spreadsheet needs a new tab.",
100
102
  annotations: WRITE,
101
103
  inputSchema: z.object({ appId, id: spreadsheetId, name: z.string() }).strict(),
102
104
  outputSchema: OUTPUT_SCHEMAS.sheets_add_sheet,
@@ -40,7 +40,7 @@ export function registerStoreReadTools(server, makeClient) {
40
40
  "shop \"your-store.myshopify.com\"). Optionally appId ties the store to one product. The " +
41
41
  "owner approves at Amazon or Shopify; nothing is connected until they do, and no " +
42
42
  "credential passes through this tool. Nooticr only asks for access to listing copy and " +
43
- "prices. Each call mints a fresh link. No cost to call.",
43
+ "prices. Each call mints a fresh link. No cost to call. Use when the user wants to connect their Amazon or Shopify store.",
44
44
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
45
45
  inputSchema: z
46
46
  .object({
@@ -60,7 +60,7 @@ export function registerStoreReadTools(server, makeClient) {
60
60
  "exact SKU; Shopify: product search), limit (up to 50), cursor from nextCursor, " +
61
61
  "productRef to read one product by SKU or GID, includeIssues, includePricing (Amazon). " +
62
62
  "For Amazon, source \"report\" reads the whole catalogue through a report: it answers " +
63
- "status in_progress with a reportId and poll arguments until done. Read-only. No cost to call.",
63
+ "status in_progress with a reportId and poll arguments until done. Read-only. No cost to call. Use when the user asks about the products in their connected store.",
64
64
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
65
65
  inputSchema: z
66
66
  .object({