@nooticr/mcp 1.26.46 → 1.26.48

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
- 86 tools, grouped by what you are trying to do. Prices are in nooticr credits and
55
+ 88 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
@@ -101,9 +101,10 @@ them caps that fan-out with an argument.
101
101
  | `unwatch_creator` | free | Drop a creator from the watchlist. |
102
102
  | `list_watchlist` | free | Who you are watching: handle, platform, note, and when you last caught up on each. |
103
103
  | `catch_up_watchlist` | 2 per creator | What everyone you watch has posted since your last catch-up. Compares against the snapshot taken last time and moves it forward, so it answers "what is new" rather than "what exists". |
104
- | `create_brand_watch` | free to call | Schedule a recurring sweep and get emailed only what is new. `kind: "mentions"` (default) is a recurring `search_mentions` sweep for `term`/`platforms`; `kind: "competitor"` is a recurring `get_user_posts` check on one creator (`handle`/`platform`), mailing only what beats their own recent median. Two calls by design: the first returns the quote (cost per run, cadence, cost per day) and a `confirmationToken`; nothing is created or charged until you call it again with `confirm: true` and that token. Each run then bills the same as calling the matching tool yourself — 2 credits per network (5 for Xiaohongshu) for mentions, a flat 2 credits for competitor. |
104
+ | `create_brand_watch` | free to call | Schedule a recurring sweep and get emailed only what is new. `kind: "mentions"` (default) is a recurring `search_mentions` sweep for `term`/`platforms`; `kind: "competitor"` is a recurring `get_user_posts` check on one creator (`handle`/`platform`), mailing only what beats their own recent median; `kind: "report"` runs `job: {tool, args}` — `niche_report` (mailed as written every run) or `discover_social_posts` (only unseen posts) — on the schedule; `kind: "portfolio"` sweeps up to 25 `terms` (your brand, products, competitors) across the same networks under one budget and mails one digest with each term's share of voice. Two calls by design: the first returns the quote (cost per run, cadence, cost per day) and a `confirmationToken`; nothing is created or charged until you call it again with `confirm: true` and that token. Each run then bills the same as calling the matching tool yourself — 2 credits per network (5 for Xiaohongshu) for mentions, a flat 2 credits for competitor, the job tool's own price for a report (3 for `niche_report`), and a mentions sweep per term for a portfolio — whose budget is split evenly and trims networks, never terms, and which refunds any term whose sweep fails. |
105
105
  | `list_brand_watches` | free | Every watch you have scheduled: term, networks, cadence, cost per run, credits spent so far, runs made, and when the next one is due. |
106
106
  | `mention_trend` | free | **What a watch has seen over time**, which nothing else here answers — every other read is about now. One point per run: when it ran, how many mentions it found, how many were new, and both counts per network, so "which network is growing" and "did it go quiet or did we stop looking" come apart. On a competitor watch each point carries the median that run measured, which turns "they beat their own median" into "and that median has doubled". Plus the mentions more than one run has seen. Free because the sweeps were billed when they ran; charging again to read what was already paid for is the thing it fixes. |
107
+ | `update_watch_portfolio` | free | Add or remove a portfolio watch's terms by `watchId`. No new confirmation, because the per-run budget you agreed to does not move — more terms split it more ways, and the reply names any network a run no longer reaches. |
107
108
  | `stop_brand_watch` | free | Stop a watch by `watchId` or `term`. Immediate — the next run does not happen and nothing more is charged. |
108
109
  | `niche_report` | 2 | Recent posts in a niche with their stats, so the dominant formats, hook patterns and the gaps nobody fills can be read off them. One `discover_social_posts` call. Use when deciding what to make. |
109
110
  | `analyze_creator_profile` | 2 | A creator's recent posts with their stats — the material of a teardown: niche, themes, hook formula, what over- and underperforms, who the audience is. One `get_user_posts` call. |
@@ -174,6 +175,7 @@ a `scanId` for the rest.
174
175
  | `check_nooticr_credits` | free | Balance and billing URL. |
175
176
  | `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. |
176
177
  | `get_tool_run` | free | One run from that history by id, with its full error. |
178
+ | `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. |
177
179
  | `nooticr_login` | free | Re-link the account when a call fails with an authentication error. |
178
180
  | `list_social_connections` | free | What you have connected and what each connection is actually allowed to do — read the account, publish a post, manage comments — as yes, no or **unknown**. "Manage comments" reports the platform's grant; no nooticr tool posts or replies to a comment. Unknown means the grant predates scope recording: treat it as "try it", not as a refusal. Also says which platforms can be linked at all, which is a smaller set than the networks nooticr reads. |
179
181
  | `connect_social_account` | free | A link the user opens to connect one account. They approve at the provider in their own browser; nothing is connected until they do, and no credential passes through the tool or the model's context. Each call mints a fresh link — an old one does not work twice. |
@@ -34,7 +34,15 @@ const CADENCE_RATE = {
34
34
  weekly: { runs: 1, per: "week" },
35
35
  };
36
36
  /** Mirrors `crate::brand_watch::WatchKind` (nooticr-server, brand_watch.rs). */
37
- const KINDS = ["mentions", "competitor"];
37
+ const KINDS = ["mentions", "competitor", "report", "portfolio"];
38
+ /**
39
+ * Mirrors `SCHEDULABLE_JOBS` in nooticr-server's brand_watch.rs: the tools a
40
+ * report watch may run. Anything else is refused there too; listing them here
41
+ * is so the enum a host sees is the list that will actually be accepted.
42
+ */
43
+ export const SCHEDULABLE_JOBS = ["niche_report", "discover_social_posts"];
44
+ /** Mirrors `MAX_PORTFOLIO_TERMS` (and the migration's check). */
45
+ const MAX_PORTFOLIO_TERMS = 25;
38
46
  /**
39
47
  * What one run of a competitor watch bills: one flat `get_user_posts` call,
40
48
  * whatever the platform. Mirrors `COMPETITOR_SWEEP_TOOL`'s cost
@@ -46,6 +54,47 @@ const KINDS = ["mentions", "competitor"];
46
54
  function competitorWatchCost() {
47
55
  return BACKEND_CALL_CREDITS.get_user_posts;
48
56
  }
57
+ /**
58
+ * What one run of a report watch bills: its job's own price, the same number
59
+ * `mcp_tool_cost_for` charges for that call by hand. A discovery job that
60
+ * opens posts for their comments pays one call per post opened.
61
+ */
62
+ function reportWatchCost(job) {
63
+ if (!job)
64
+ return 0;
65
+ if (job.tool === "niche_report")
66
+ return NICHE_REPORT_CREDITS;
67
+ const base = BACKEND_CALL_CREDITS.discover_social_posts;
68
+ if (!job.args?.includeComments)
69
+ return base;
70
+ const opened = Math.min(10, Math.max(1, Math.trunc(job.args.openPosts ?? 5)));
71
+ return base + opened;
72
+ }
73
+ /** `niche_report`'s price in `mcp_tool_cost` — text-only AI, no video. */
74
+ const NICHE_REPORT_CREDITS = 3;
75
+ /** The terms a portfolio watch sweeps, as the server will read them. */
76
+ function portfolioTerms(terms) {
77
+ const out = [];
78
+ for (const raw of terms ?? []) {
79
+ const t = raw.trim();
80
+ if (t && !out.some((o) => o.toLowerCase() === t.toLowerCase()))
81
+ out.push(t);
82
+ }
83
+ return out;
84
+ }
85
+ /** An upper bound on one run of any kind, before the budget ceiling. */
86
+ function sweepCost(args) {
87
+ switch (args.kind ?? "mentions") {
88
+ case "competitor":
89
+ return competitorWatchCost();
90
+ case "report":
91
+ return reportWatchCost(args.job);
92
+ case "portfolio":
93
+ return searchMentionsCost(args.platforms) * Math.max(1, portfolioTerms(args.terms).length);
94
+ default:
95
+ return searchMentionsCost(args.platforms);
96
+ }
97
+ }
49
98
  /**
50
99
  * The human-facing half of the quote-then-confirm protocol.
51
100
  *
@@ -70,7 +119,7 @@ async function gateRecurringCharge(server, args) {
70
119
  // means a competitor watch's single 2-credit call always fits, so there is
71
120
  // nothing to trim in practice — unlike a mentions sweep, which frequently
72
121
  // does.
73
- const sweep = kind === "competitor" ? competitorWatchCost() : searchMentionsCost(args.platforms);
122
+ const sweep = sweepCost(args);
74
123
  // An upper bound on the run, deliberately, rather than the exact figure.
75
124
  // The server does not bill the budget: `plan_run` drops whole platforms
76
125
  // until the rest fit under it, so a ceiling of 9 against 2-credit networks
@@ -81,10 +130,22 @@ async function gateRecurringCharge(server, args) {
81
130
  const rate = CADENCE_RATE[args.cadence ?? "daily"];
82
131
  const perPeriod = perRun * rate.runs;
83
132
  const where = args.deliverTo ? ` Its digest goes to ${args.deliverTo}.` : "";
84
- const label = kind === "competitor" ? `@${args.handle} on ${args.platform ?? "?"}` : `"${args.term}"`;
133
+ const every = (args.cadence ?? "daily").replace(/_/g, " ");
134
+ const terms = portfolioTerms(args.terms);
135
+ const label = kind === "competitor"
136
+ ? `@${args.handle} on ${args.platform ?? "?"}`
137
+ : kind === "report"
138
+ ? `"${args.job?.args?.niche ?? "?"}" with ${args.job?.tool ?? "?"}`
139
+ : kind === "portfolio"
140
+ ? `${terms.length} term${terms.length === 1 ? "" : "s"} (${terms.join(", ")})`
141
+ : `"${args.term}"`;
85
142
  const what = kind === "competitor"
86
- ? `email what beats their own median, ${(args.cadence ?? "daily").replace(/_/g, " ")}`
87
- : `email what is new, ${(args.cadence ?? "daily").replace(/_/g, " ")}`;
143
+ ? `email what beats their own median, ${every}`
144
+ : kind === "report"
145
+ ? `email the result, ${every}`
146
+ : kind === "portfolio"
147
+ ? `email one digest with each term's share of voice, ${every}`
148
+ : `email what is new, ${every}`;
88
149
  const decision = await confirmSpend(server.server, {
89
150
  credits: perPeriod,
90
151
  // Not a number worth weighing against a dialog — a standing charge. See
@@ -102,9 +163,12 @@ async function gateRecurringCharge(server, args) {
102
163
  // Said plainly, because the first model to meet this read "cancelled" as
103
164
  // a server fault and reported the feature broken — one retry away from
104
165
  // treating a person's "no" as an obstacle to route around.
105
- const oneOff = kind === "competitor"
106
- ? `A one-off get_user_posts call costs ${perRun} credits and repeats never.`
107
- : `A one-off search_mentions sweep costs ${perRun} credits and repeats never.`;
166
+ const oneOffTool = kind === "competitor"
167
+ ? "get_user_posts call"
168
+ : kind === "report"
169
+ ? `${args.job?.tool ?? "report"} call`
170
+ : "search_mentions sweep";
171
+ const oneOff = `A one-off ${oneOffTool} costs ${kind === "portfolio" ? searchMentionsCost(args.platforms) : perRun} credits${kind === "portfolio" ? " per term" : ""} and repeats never.`;
108
172
  return declinedResult(perPeriod, `That watch`, `Nothing was created, and nothing is wrong: the person paying declined the recurring ` +
109
173
  `charge, or could not be shown it. Calling again with the same token will reach the ` +
110
174
  `same answer — ask them directly instead. ${oneOff}`);
@@ -199,6 +263,36 @@ function perPlatformDirection(runs) {
199
263
  "purpose — `runs` is newest-first, so subtracting the ends by hand is where a real reading " +
200
264
  "of this went backwards. Use these rather than re-deriving them.");
201
265
  }
266
+ /**
267
+ * A portfolio's share of voice, newest run against oldest — computed here for
268
+ * the same reason `perPlatformDirection` is: `runs` is newest-first.
269
+ */
270
+ function portfolioShare(runs) {
271
+ const at = (r) => String(r.ranAt ?? "");
272
+ const chronological = [...runs].sort((a, b) => (at(a) < at(b) ? -1 : 1));
273
+ const shareOf = (r) => (r?.perTerm ?? {});
274
+ const latest = shareOf(chronological[chronological.length - 1]);
275
+ const earliest = shareOf(chronological[0]);
276
+ const terms = Object.keys(latest);
277
+ if (!terms.length) {
278
+ return ("This is a portfolio watch, but no run in this window recorded per-term counts, so there " +
279
+ "is no share of voice to report yet.");
280
+ }
281
+ const said = terms
282
+ .map((t) => {
283
+ const now = latest[t];
284
+ if (now?.error)
285
+ return `${t}: not searched in the latest run (its sweep failed and was refunded)`;
286
+ const then = earliest[t];
287
+ const was = chronological.length > 1 && typeof then?.share === "number" ? `, from ${then.share}%` : "";
288
+ return `${t}: ${now?.share ?? 0}% of mentions found (${now?.found ?? 0})${was}`;
289
+ })
290
+ .join("; ");
291
+ return (`This is a portfolio watch, so each point also carries \`perTerm\` — every term's found and new ` +
292
+ `counts and its share of all mentions found that run. Latest run: ${said}. A share is only ` +
293
+ "comparable between terms because every term was searched on the same networks; a term " +
294
+ "added to the portfolio part-way through has no share before it was added, which is not zero.");
295
+ }
202
296
  function trendGuidance(sc, runs) {
203
297
  const n = runs.length;
204
298
  const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null);
@@ -252,6 +346,9 @@ function trendGuidance(sc, runs) {
252
346
  (created ? ` This watch was created ${created.slice(0, 10)}.` : "") +
253
347
  " A series that starts flat starts where the record does, which is not the same as a " +
254
348
  "period when nobody was talking.");
349
+ if (sc.kind === "portfolio") {
350
+ lines.push("", portfolioShare(runs));
351
+ }
255
352
  const recurring = Array.isArray(sc.recurring) ? sc.recurring : [];
256
353
  if (recurring.length) {
257
354
  lines.push("", `\`recurring\` lists the ${recurring.length} mention${recurring.length === 1 ? "" : "s"} more ` +
@@ -293,13 +390,21 @@ export function registerBrandWatch(server, makeClient) {
293
390
  server.registerTool("create_brand_watch", {
294
391
  title: "Create Brand Watch",
295
392
  description: "Run a sweep on a schedule and email the user what is new, instead of them remembering to " +
296
- "ask. Two kinds, chosen with kind: kind: \"mentions\" (the default — omit kind entirely for " +
393
+ "ask. Four kinds, chosen with kind: kind: \"mentions\" (the default — omit kind entirely for " +
297
394
  "this one) runs a brand-mentions sweep for term across platforms; kind: \"competitor\" runs " +
298
395
  "a get_user_posts sweep for one creator (handle + platform) and reports only the posts that " +
299
396
  "beat that creator's own recent median — a raw view count would just measure follower size, " +
300
- "so a run only mails what is unusually good for them. term/platforms belong to a mentions " +
301
- "watch; handle/platform belong to a competitor watch — pass the pair that matches kind and " +
302
- "leave the other pair out. Two calls by design, because this starts a charge that recurs " +
397
+ "so a run only mails what is unusually good for them; kind: \"report\" runs one tool on the " +
398
+ "schedule, given as job: {tool, args} — niche_report (the report is mailed as written every " +
399
+ "run) or discover_social_posts (only posts no earlier run mailed) — billed at that tool's " +
400
+ "own price; kind: \"portfolio\" sweeps several terms at once (a brand, its products and its " +
401
+ "competitors, up to 25) across the same platforms under one budget and mails one digest with " +
402
+ "each term's share of voice — the budget is split evenly across the terms and trims " +
403
+ "networks, never terms, and a term whose sweep fails is refunded. Add or remove a " +
404
+ "portfolio's terms later with update_watch_portfolio. term/platforms belong to a mentions " +
405
+ "watch; handle/platform belong to a competitor watch; job to a report; terms/platforms (and " +
406
+ "optionally term, as the portfolio's name) to a portfolio — pass what matches kind and " +
407
+ "leave the rest out. Two calls by design, because this starts a charge that recurs " +
303
408
  "while nobody is watching: call it once with no confirmation to get back the cost per run, " +
304
409
  "the cadence and what those multiply out to per day, put those numbers to the user in your " +
305
410
  "reply, and only then call it again with confirm: true and the confirmationToken you were " +
@@ -311,7 +416,9 @@ export function registerBrandWatch(server, makeClient) {
311
416
  "between them and a standing charge. Each run bills exactly what the same call costs when a " +
312
417
  "person asks for it themselves: for a mentions watch, 2 credits per network swept, 5 for " +
313
418
  "Xiaohongshu; for a competitor watch, a flat 2 credits (one get_user_posts call), whatever " +
314
- "the platform. budgetCredits is a hard per-run ceiling enforced on the server, not a " +
419
+ "the platform; for a report, the job tool's own price (niche_report 3, discover_social_posts " +
420
+ "2, plus 1 per post opened for comments); for a portfolio, a mentions sweep per term. " +
421
+ "budgetCredits is a hard per-run ceiling enforced on the server, not a " +
315
422
  "suggestion — a mentions sweep that would cost more is trimmed to the networks that fit, " +
316
423
  "never widened. A run that turns up nothing new — or, for a competitor watch, nothing above " +
317
424
  "median — sends no mail. cadence is hourly, every_6_hours, every_12_hours, daily or weekly, " +
@@ -322,8 +429,8 @@ export function registerBrandWatch(server, makeClient) {
322
429
  kind: z
323
430
  .enum(KINDS)
324
431
  .optional()
325
- .describe("\"mentions\" (default) or \"competitor\". Selects which of the two argument pairs " +
326
- "below applies."),
432
+ .describe("\"mentions\" (default), \"competitor\", \"report\" or \"portfolio\". Selects which " +
433
+ "of the arguments below apply."),
327
434
  term: z
328
435
  .string()
329
436
  .optional()
@@ -346,6 +453,39 @@ export function registerBrandWatch(server, makeClient) {
346
453
  .optional()
347
454
  .describe("Required for kind: \"competitor\" — the single network that creator posts on, e.g. " +
348
455
  "\"tiktok\". Not used for a mentions watch."),
456
+ job: z
457
+ .object({
458
+ tool: z
459
+ .enum(SCHEDULABLE_JOBS)
460
+ .describe("The tool to run on the schedule. Only these two can be scheduled."),
461
+ args: z
462
+ .object({
463
+ niche: z.string().describe("Niche or topic, e.g. \"home fitness\"."),
464
+ platform: z.string().optional().describe("Platform to read (the tool's own default when omitted)."),
465
+ count: z.number().int().optional().describe("niche_report only: posts to survey (default 20, max 40)."),
466
+ keywords: z.string().optional().describe("discover_social_posts only: extra keywords."),
467
+ limit: z.number().int().optional().describe("discover_social_posts only: max posts (default 6)."),
468
+ includeComments: z
469
+ .boolean()
470
+ .optional()
471
+ .describe("discover_social_posts only: also read comments — 1 extra credit per post opened, every run."),
472
+ openPosts: z.number().int().optional().describe("discover_social_posts only: posts to open for comments (default 5, max 10)."),
473
+ commentsPerPost: z.number().int().optional().describe("discover_social_posts only: comments per opened post."),
474
+ })
475
+ .strict()
476
+ .describe("The arguments the tool would take by hand. Pagination is not schedulable."),
477
+ })
478
+ .strict()
479
+ .optional()
480
+ .describe("Required for kind: \"report\" — the job each run performs. Not used for other kinds."),
481
+ terms: z
482
+ .array(z.string())
483
+ .min(1)
484
+ .max(MAX_PORTFOLIO_TERMS)
485
+ .optional()
486
+ .describe("Required for kind: \"portfolio\" — the terms to compare, e.g. your brand, your " +
487
+ "products and your competitors (1-25, duplicates ignored). Each is swept across " +
488
+ "platforms. Not used for other kinds; for a portfolio, term is its optional name."),
349
489
  cadence: z.enum(CADENCES).optional().describe("How often to run. Defaults to daily."),
350
490
  budgetCredits: z
351
491
  .number()
@@ -494,6 +634,59 @@ export function registerBrandWatch(server, makeClient) {
494
634
  return failed("mention_trend failed", err);
495
635
  }
496
636
  });
637
+ server.registerTool("update_watch_portfolio", {
638
+ title: "Update Watch Portfolio",
639
+ description: "Add terms to a portfolio brand watch, or remove them, by watchId, with add and/or remove " +
640
+ "lists. No new confirmation: the per-run budget and cadence the user agreed to do not " +
641
+ "change, so a run still never costs more than that ceiling — but more terms split the same " +
642
+ "budget more ways, so the response says which networks a run will still search. If it " +
643
+ "names any in platformsSkipped, tell the user those networks are no longer searched and " +
644
+ "offer to remove a term or recreate the watch with a larger budget. A change the budget " +
645
+ "cannot cover at all is refused and nothing changes. No cost to call: no credits are spent.",
646
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
647
+ inputSchema: z
648
+ .object({
649
+ watchId: z.string().describe("The portfolio watch's id, from list_brand_watches."),
650
+ add: z.array(z.string()).optional().describe("Terms to add (duplicates ignored)."),
651
+ remove: z
652
+ .array(z.string())
653
+ .optional()
654
+ .describe("Terms to take out, matched without regard to case."),
655
+ })
656
+ .strict(),
657
+ outputSchema: OUTPUT_SCHEMAS.update_watch_portfolio,
658
+ }, async (args, extra) => {
659
+ const client = await makeClient({ ...extra, arguments: args });
660
+ try {
661
+ const result = toResult(await client.callTool("update_watch_portfolio", args));
662
+ const sc = (result.structuredContent ?? {});
663
+ if (!sc.updated)
664
+ return result;
665
+ // Both channels (#44, #51): a host that renders structuredContent drops
666
+ // the text block, and one that shows only text never sees the payload —
667
+ // and a network the split budget stopped reaching is the one thing
668
+ // here the user has to be told.
669
+ const skipped = Array.isArray(sc.platformsSkipped) ? sc.platformsSkipped : [];
670
+ const terms = Array.isArray(sc.terms) ? sc.terms : [];
671
+ const guidance = `The portfolio now watches ${terms.length} term${terms.length === 1 ? "" : "s"}: ` +
672
+ `${terms.join(", ")}. A run costs ${sc.costPerRun ?? "?"} credits, within the ` +
673
+ `${sc.budgetPerRun ?? "?"}-credit ceiling already agreed, and searches ` +
674
+ `${(Array.isArray(sc.platformsSearched) ? sc.platformsSearched : []).join(", ") || "no network"}.` +
675
+ (skipped.length
676
+ ? ` Split this many ways, the budget no longer reaches ${skipped.join(", ")} — tell the ` +
677
+ "user those networks are not searched for any term now, and offer to remove a term " +
678
+ "or recreate the watch with a larger budget if they matter."
679
+ : "");
680
+ const payload = { guidance, ...sc };
681
+ return {
682
+ content: [{ type: "text", text: guidance }],
683
+ structuredContent: payload,
684
+ };
685
+ }
686
+ catch (err) {
687
+ return failed("update_watch_portfolio failed", err);
688
+ }
689
+ });
497
690
  server.registerTool("stop_brand_watch", {
498
691
  title: "Stop Brand Watch",
499
692
  description: "Stop a scheduled brand-monitoring watch, by watchId or by term. Takes effect immediately: " +
@@ -474,6 +474,7 @@ export const LOADING_PLANS = {
474
474
  create_brand_watch: { label: "Setting up the watch", kind: "text", n: 1, steps: [], free: true },
475
475
  list_brand_watches: { label: "Reading your watches", kind: "list", n: 3, steps: [], free: true },
476
476
  stop_brand_watch: { label: "Stopping the watch", kind: "text", n: 1, steps: [], free: true },
477
+ update_watch_portfolio: { label: "Updating the portfolio", kind: "text", n: 1, steps: [], free: true },
477
478
  // ─── Own-account reads: nooticr's own stored rows, never an upstream call ───
478
479
  list_own_apps: { label: "Reading your products", kind: "list", n: 3, steps: [], free: true },
479
480
  get_content_plan: { label: "Reading the saved plan", kind: "text", n: 1, steps: [], free: true },
@@ -495,6 +496,7 @@ export const LOADING_PLANS = {
495
496
  check_nooticr_credits: { label: "Checking your balance", kind: "text", n: 1, steps: [], free: true },
496
497
  list_tool_runs: { label: "Reading your run history", kind: "text", n: 1, steps: [], free: true },
497
498
  get_tool_run: { label: "Reading the run", kind: "text", n: 1, steps: [], free: true },
499
+ get_usage_report: { label: "Adding up the run ledger", kind: "text", n: 1, steps: [], free: true },
498
500
  nooticr_getting_started: { label: "Reading where your account stands", kind: "text", n: 1, steps: [], free: true },
499
501
  nooticr_login: { label: "Getting a sign-in link", kind: "text", n: 1, steps: [], free: true },
500
502
  watch_creator: { label: "Adding to your watchlist", kind: "text", n: 1, steps: [], free: true },
@@ -1135,6 +1135,23 @@ export const OUTPUT_SCHEMAS = {
1135
1135
  userId: scalar().describe("Whose run it was; only in the workspace scope."),
1136
1136
  }).nullish(),
1137
1137
  }),
1138
+ get_usage_report: open({
1139
+ report: scalar(),
1140
+ scope: scalar().describe("mine or workspace."),
1141
+ from: scalar(),
1142
+ to: scalar(),
1143
+ totals: open({
1144
+ calls: scalar(),
1145
+ failures: scalar(),
1146
+ credits: scalar().describe("Credits actually taken: free first uses, refunds and replays are zero."),
1147
+ tools: scalar(),
1148
+ seats: scalar(),
1149
+ }).nullish(),
1150
+ byTool: listOf(open({ tool: scalar(), calls: scalar(), failures: scalar(), credits: scalar(), lastUsedAt: scalar() })),
1151
+ bySeat: listOf(open({ userId: scalar(), email: scalar(), name: scalar(), calls: scalar(), failures: scalar(), credits: scalar() })).describe("Workspace reports only; empty on a personal one."),
1152
+ byDay: listOf(open({ day: scalar(), calls: scalar(), failures: scalar(), credits: scalar() })),
1153
+ recentFailures: listOf(open({ id: scalar(), tool: scalar(), error: scalar(), startedAt: scalar(), userId: scalar(), credits: scalar() })),
1154
+ }),
1138
1155
  nooticr_login: open({
1139
1156
  signedIn: scalar().describe("true when the session is already good and no link is needed."),
1140
1157
  loginUrl: scalar().describe("Only present when a sign-in is actually required."),
@@ -1148,9 +1165,11 @@ export const OUTPUT_SCHEMAS = {
1148
1165
  confirmationToken: scalar().describe("Pass this back with confirm: true to actually create the watch."),
1149
1166
  expiresInSeconds: scalar(),
1150
1167
  quote: open({
1151
- kind: scalar().describe("\"mentions\" or \"competitor\"."),
1152
- term: scalar().describe("The search term (mentions) or the creator handle (competitor)."),
1153
- platforms: listOf(z.string()).describe("Networks swept (mentions), or the creator's single platform (competitor)."),
1168
+ kind: scalar().describe("\"mentions\", \"competitor\", \"report\" or \"portfolio\"."),
1169
+ term: scalar().describe("The search term (mentions), the creator handle (competitor), or the watch's name (report, portfolio)."),
1170
+ platforms: listOf(z.string()).describe("Networks swept (mentions, portfolio), or the creator's single platform (competitor)."),
1171
+ job: anyObject().nullish().describe("Report only: the {tool, args} each run performs, as it will run."),
1172
+ terms: listOf(z.string()).describe("Portfolio only: the terms each run sweeps."),
1154
1173
  cadence: scalar(),
1155
1174
  cadenceMinutes: scalar(),
1156
1175
  costPerRun: scalar(),
@@ -1166,11 +1185,13 @@ export const OUTPUT_SCHEMAS = {
1166
1185
  rejected: scalar().describe("Set when confirm was true but the token didn't check out — nothing was created."),
1167
1186
  alreadyWatching: scalar().describe("Set when this term already has an enabled watch — creating another would double-charge."),
1168
1187
  watchId: scalar(),
1169
- kind: scalar().describe("\"mentions\" or \"competitor\"."),
1188
+ kind: scalar().describe("\"mentions\", \"competitor\", \"report\" or \"portfolio\"."),
1170
1189
  term: scalar(),
1171
1190
  platforms: listOf(z.string()),
1172
1191
  competitorHandle: scalar().describe("Set only for a competitor watch."),
1173
1192
  competitorPlatform: scalar().describe("Set only for a competitor watch."),
1193
+ job: anyObject().nullish().describe("Set only for a report watch."),
1194
+ terms: listOf(z.string()).describe("Set only for a portfolio watch."),
1174
1195
  cadence: scalar(),
1175
1196
  costPerRun: scalar(),
1176
1197
  creditsPerDay: scalar(),
@@ -1202,6 +1223,7 @@ export const OUTPUT_SCHEMAS = {
1202
1223
  found: scalar().describe("Everything that sweep saw."),
1203
1224
  reported: scalar().describe("The subset that was new — a different question from found."),
1204
1225
  perPlatform: anyObject().nullish().describe('{"tiktok":{"found":12,"reported":3},...}'),
1226
+ perTerm: anyObject().nullish().describe('Portfolio watches only: {"acme":{"found":9,"reported":2,"share":60},...}'),
1205
1227
  medianViews: scalar().describe("Competitor watches only: the baseline that run measured."),
1206
1228
  postsScored: scalar(),
1207
1229
  costCredits: scalar(),
@@ -1211,6 +1233,7 @@ export const OUTPUT_SCHEMAS = {
1211
1233
  term: scalar(),
1212
1234
  competitorHandle: scalar(),
1213
1235
  platforms: listOf(z.string()),
1236
+ terms: listOf(z.string()).describe("Portfolio watches only."),
1214
1237
  windowDays: scalar(),
1215
1238
  // Two numbers a caller cannot derive and will otherwise assume: how long
1216
1239
  // anything is kept, and how long the watch has existed. A flat left edge
@@ -1233,11 +1256,13 @@ export const OUTPUT_SCHEMAS = {
1233
1256
  list_brand_watches: open({
1234
1257
  watches: listOf(open({
1235
1258
  watchId: scalar(),
1236
- kind: scalar().describe("\"mentions\" or \"competitor\"."),
1259
+ kind: scalar().describe("\"mentions\", \"competitor\", \"report\" or \"portfolio\"."),
1237
1260
  term: scalar(),
1238
1261
  platforms: listOf(z.string()),
1239
1262
  competitorHandle: scalar().describe("Set only for a competitor watch."),
1240
1263
  competitorPlatform: scalar().describe("Set only for a competitor watch."),
1264
+ job: anyObject().nullish().describe("Set only for a report watch: the {tool, args} each run performs."),
1265
+ terms: listOf(z.string()).describe("Set only for a portfolio watch."),
1241
1266
  cadence: scalar(),
1242
1267
  costPerRun: scalar(),
1243
1268
  budgetPerRun: scalar(),
@@ -1253,6 +1278,20 @@ export const OUTPUT_SCHEMAS = {
1253
1278
  creditsPerDayAcrossAllWatches: scalar().describe("What every enabled watch together will cost per day if nothing changes."),
1254
1279
  cost: scalar(),
1255
1280
  }),
1281
+ update_watch_portfolio: open({
1282
+ guidance,
1283
+ updated: scalar(),
1284
+ watchId: scalar(),
1285
+ term: scalar(),
1286
+ terms: listOf(z.string()),
1287
+ platformsSearched: listOf(z.string()).describe("What a run will search now, for every term."),
1288
+ platformsSkipped: listOf(z.string()).describe("Networks the split budget no longer reaches — say so to the user."),
1289
+ costPerRun: scalar(),
1290
+ budgetPerRun: scalar(),
1291
+ cadence: scalar(),
1292
+ message: scalar(),
1293
+ cost: scalar(),
1294
+ }),
1256
1295
  stop_brand_watch: open({
1257
1296
  stopped: scalar(),
1258
1297
  alreadyStopped: scalar(),
@@ -222,6 +222,12 @@ export const TOOL_DEFINITIONS = [
222
222
  description: "One run from list_tool_runs by its id, with its full error text, credentials scrubbed. A run outside your scope reads as not found. No cost to call. Use to look closely at one charge.",
223
223
  inputSchema: z.object({ id: z.number().int(), scope: z.enum(["mine", "workspace"]).optional() }).strict(),
224
224
  },
225
+ {
226
+ name: "get_usage_report",
227
+ title: "Get Usage Report",
228
+ description: "A usage report over a window: total calls, failures and credits actually taken, per tool, per day, and recent failures; scope workspace adds one row per seat, for owners and admins. No cost to call.",
229
+ inputSchema: z.object({ days: z.number().int().min(1).max(366).optional(), from: z.string().optional(), to: z.string().optional(), scope: z.enum(["mine", "workspace"]).optional() }).strict(),
230
+ },
225
231
  {
226
232
  name: "nooticr_login",
227
233
  title: "Nooticr Login",
@@ -255,8 +261,8 @@ export const TOOL_DEFINITIONS = [
255
261
  {
256
262
  name: "create_brand_watch",
257
263
  title: "Create Brand Watch",
258
- description: "Run a sweep on a schedule and email what is new, instead of remembering to ask. kind: \"mentions\" (default) sweeps term across platforms; kind: \"competitor\" sweeps one creator's own posts (handle + platform) and reports only what beats their own recent median. Two calls by design: call it once with no confirmation to get back the cost per run and what it multiplies out to per day, then call it again with confirm: true and the confirmationToken to actually start it. The first call creates nothing and spends nothing. Bills 2 credits per network per run (5 for Xiaohongshu) for a mentions watch, or a flat 2 credits for a competitor watch, trimmed to fit budgetCredits. A run that finds nothing new (or, for a competitor watch, nothing above median) sends no mail. No cost to call.",
259
- inputSchema: z.object({ kind: z.enum(["mentions", "competitor"]).optional().describe("\"mentions\" (default) or \"competitor\"."), term: z.string().optional().describe("kind: mentions only — what to watch for (max 120 chars)."), platforms: z.array(z.string()).optional().describe("kind: mentions only — networks to sweep (default: every searchable network)."), handle: z.string().optional().describe("kind: competitor only — the creator's handle."), platform: z.string().optional().describe("kind: competitor only — the platform that creator posts on."), cadence: z.enum(["hourly", "every_6_hours", "every_12_hours", "daily", "weekly"]).optional().describe("How often to run (default daily)."), budgetCredits: z.number().int().optional().describe("Hard per-run credit ceiling (default the full sweep's cost)."), deliverTo: z.string().optional().describe("Digest email (default the account's own)."), confirm: z.boolean().optional().describe("Set true only once the user has agreed to the quoted cost."), confirmationToken: z.string().optional().describe("The token the first call returned.") }).strict(),
264
+ description: "Run a sweep on a schedule and email what is new, instead of remembering to ask. kind: \"mentions\" (default) sweeps term across platforms; kind: \"competitor\" sweeps one creator's own posts (handle + platform) and reports only what beats their own recent median; kind: \"report\" runs job {tool, args} (niche_report or discover_social_posts) on the schedule at that tool's own price; kind: \"portfolio\" sweeps several terms across the same platforms under one budget and mails one digest with each term's share of voice. Two calls by design: call it once with no confirmation to get back the cost per run and what it multiplies out to per day, then call it again with confirm: true and the confirmationToken to actually start it. The first call creates nothing and spends nothing. Bills 2 credits per network per run (5 for Xiaohongshu) for a mentions watch, or a flat 2 credits for a competitor watch, trimmed to fit budgetCredits. A run that finds nothing new (or, for a competitor watch, nothing above median) sends no mail. No cost to call.",
265
+ inputSchema: z.object({ kind: z.enum(["mentions", "competitor", "report", "portfolio"]).optional().describe("\"mentions\" (default), \"competitor\", \"report\" or \"portfolio\"."), job: z.object({ tool: z.enum(["niche_report", "discover_social_posts"]), args: z.object({}).passthrough() }).optional().describe("kind: report only — the job each run performs."), terms: z.array(z.string()).optional().describe("kind: portfolio only — the terms to compare (1-25)."), term: z.string().optional().describe("kind: mentions only — what to watch for (max 120 chars)."), platforms: z.array(z.string()).optional().describe("kind: mentions only — networks to sweep (default: every searchable network)."), handle: z.string().optional().describe("kind: competitor only — the creator's handle."), platform: z.string().optional().describe("kind: competitor only — the platform that creator posts on."), cadence: z.enum(["hourly", "every_6_hours", "every_12_hours", "daily", "weekly"]).optional().describe("How often to run (default daily)."), budgetCredits: z.number().int().optional().describe("Hard per-run credit ceiling (default the full sweep's cost)."), deliverTo: z.string().optional().describe("Digest email (default the account's own)."), confirm: z.boolean().optional().describe("Set true only once the user has agreed to the quoted cost."), confirmationToken: z.string().optional().describe("The token the first call returned.") }).strict(),
260
266
  },
261
267
  {
262
268
  name: "list_brand_watches",
@@ -282,6 +288,12 @@ export const TOOL_DEFINITIONS = [
282
288
  description: "Stop a scheduled brand-monitoring watch, by watchId or by term. Takes effect immediately — the run that was due does not happen and nothing further is charged. Works even at a zero balance, since a user out of credits is exactly the user who needs to turn off what is spending them. No cost to call.",
283
289
  inputSchema: z.object({ watchId: z.string().optional().describe("The watch's id, from list_brand_watches."), term: z.string().optional().describe("Alternative to watchId — the term it was watching.") }).strict(),
284
290
  },
291
+ {
292
+ name: "update_watch_portfolio",
293
+ title: "Update Watch Portfolio",
294
+ description: "Add terms to a portfolio brand watch or remove them, by watchId. No new confirmation: the agreed per-run budget and cadence do not change, so a run never costs more credits than that ceiling — more terms split it more ways, and the response names any networks a run no longer reaches. A change the budget cannot cover is refused. No cost to call.",
295
+ inputSchema: z.object({ watchId: z.string().describe("The portfolio watch's id, from list_brand_watches."), add: z.array(z.string()).optional().describe("Terms to add."), remove: z.array(z.string()).optional().describe("Terms to take out.") }).strict(),
296
+ },
285
297
  {
286
298
  name: "list_own_apps",
287
299
  title: "List Own Apps",
@@ -30,7 +30,7 @@ import { registerCollabTools } from "./collab.js";
30
30
  import { registerAmazonTools } from "./amazon.js";
31
31
  import { registerMarketplaceTools } from "./marketplace.js";
32
32
  /** Current MCP server version — bumped on every deploy for traceability. */
33
- export const MCP_SERVER_VERSION = "1.26.46";
33
+ export const MCP_SERVER_VERSION = "1.26.48";
34
34
  /** MCP Apps extension identifier */
35
35
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
36
36
  /** MIME type for MCP Apps HTML resources */
@@ -604,6 +604,7 @@ export function createMcpServer(rawMakeClient, opts) {
604
604
  "check_nooticr_credits",
605
605
  "list_tool_runs",
606
606
  "get_tool_run",
607
+ "get_usage_report",
607
608
  "list_watchlist",
608
609
  "nooticr_getting_started",
609
610
  "understand_social_post",
@@ -2610,6 +2611,43 @@ export function createMcpServer(rawMakeClient, opts) {
2610
2611
  return toolError("get_tool_run failed", err);
2611
2612
  }
2612
2613
  });
2614
+ // The run ledger added up (#93): the question an owner or a security review
2615
+ // asks ("what did the team run this month, what did it cost, what failed")
2616
+ // answered from the same rows as the history, never from memory.
2617
+ server.registerTool("get_usage_report", {
2618
+ title: "Get Usage Report",
2619
+ description: "A usage report over a window: total calls, failures and credits actually taken; one row " +
2620
+ "per tool, most expensive first; one row per day, quiet days included; and the ten most " +
2621
+ "recent failures with their errors, credentials scrubbed. scope: \"workspace\" covers " +
2622
+ "every member of your workspace and adds one row per seat; it is for the workspace's " +
2623
+ "owners and admins. Reads the stored run ledger, so it is free and says what happened, " +
2624
+ "not what is running. No cost to call. Use for \"what did we spend this month and on " +
2625
+ "what\" or an audit of what nooticr did on the workspace's behalf; list_tool_runs is the " +
2626
+ "call-by-call view.",
2627
+ _meta: {
2628
+ ui: { resourceUri: uiResource("get_usage_report") },
2629
+ "ui/resourceUri": uiResource("get_usage_report"),
2630
+ "openai/outputTemplate": appsSdkResource("get_usage_report"),
2631
+ },
2632
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
2633
+ outputSchema: OUTPUT_SCHEMAS.get_usage_report,
2634
+ inputSchema: z
2635
+ .object({
2636
+ days: z.number().int().min(1).max(366).optional().describe("The window, in days back from now (default 30)."),
2637
+ from: z.string().optional().describe("Start of the window, ISO date or timestamp. Overrides days."),
2638
+ to: z.string().optional().describe("End of the window, ISO date or timestamp (default now)."),
2639
+ scope: runFilters.scope,
2640
+ })
2641
+ .strict(),
2642
+ }, async (args, extra) => {
2643
+ const client = await makeClient({ ...extra, arguments: args });
2644
+ try {
2645
+ return await toToolResult(await client.callTool("get_usage_report", { ...args }));
2646
+ }
2647
+ catch (err) {
2648
+ return toolError("get_usage_report failed", err);
2649
+ }
2650
+ });
2613
2651
  server.registerTool("nooticr_login", {
2614
2652
  title: "Nooticr Login",
2615
2653
  description: "Get a fresh login URL to re-authenticate your MCP session. Call this tool when you need to reconnect or when the session has expired. No cost to call." +
@@ -45,6 +45,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
45
45
  check_nooticr_credits:"Check Credits",
46
46
  list_tool_runs:"Tool Runs",
47
47
  get_tool_run:"Tool Run",
48
+ get_usage_report:"Usage Report",
48
49
  list_watchlist:"Watchlist",
49
50
  detect_spoken_mentions:"Spoken Mentions",
50
51
  nooticr_getting_started:"Getting Started",
@@ -84,6 +85,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
84
85
  check_nooticr_credits:"View your Nooticr credit balance and usage.",
85
86
  list_tool_runs:"Where your credits went: every call, what it cost, and whether it worked.",
86
87
  get_tool_run:"One call from your history, with its full error.",
88
+ get_usage_report:"What was spent, on what, by whom, and what failed.",
87
89
  list_watchlist:"The creators you are watching, and when you last caught up on each.",
88
90
  detect_spoken_mentions:"Brands named out loud in a video, beside what its caption says.",
89
91
  nooticr_getting_started:"Where your account stands and what to try first.",
@@ -3152,6 +3154,48 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
3152
3154
  return '<div class="fade-in">'+head+body+"</div>";
3153
3155
  }
3154
3156
 
3157
+ // ─── Usage report (get_usage_report) ───
3158
+ //
3159
+ // A spend table: the totals first, then where it went. Credits are what was
3160
+ // actually taken, so a free first use or a refund reads as 0, not as the
3161
+ // tool's price.
3162
+ function renderUsageReport(d){
3163
+ var t=d.totals||{};
3164
+ function tile(label,val){
3165
+ return '<div style="flex:1;min-width:110px;padding:10px 12px;border:1px solid var(--border);border-radius:10px">'
3166
+ +'<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">'+esc(label)+"</div>"
3167
+ +'<div style="font-size:18px;font-weight:700;margin-top:3px">'+esc(String(val==null?"—":val))+"</div></div>";
3168
+ }
3169
+ function table(title,cols,rows){
3170
+ if(!rows.length)return "";
3171
+ return '<div class="sec-label" style="margin-top:14px">'+esc(title)+"</div>"
3172
+ +'<table style="width:100%;border-collapse:collapse;font-size:13px">'
3173
+ +"<tr>"+cols.map(function(c,i){return '<th style="text-align:'+(i?"right":"left")+';font-weight:600;color:var(--muted);padding:4px 0">'+esc(c)+"</th>";}).join("")+"</tr>"
3174
+ +rows.map(function(r){return '<tr data-usage-row style="border-top:1px solid var(--border)">'+r.map(function(v,i){return '<td style="padding:6px 0;text-align:'+(i?"right":"left")+'">'+esc(String(v))+"</td>";}).join("")+"</tr>";}).join("")
3175
+ +"</table>";
3176
+ }
3177
+ var from=String(d.from||"").slice(0,10),to=String(d.to||"").slice(0,10);
3178
+ var days=(d.byDay||[]),max=1;
3179
+ days.forEach(function(x){max=Math.max(max,Number(x.credits)||0);});
3180
+ var bars=days.length?'<div class="sec-label" style="margin-top:14px">Credits per day</div><div style="display:flex;align-items:flex-end;gap:2px;height:48px">'
3181
+ +days.map(function(x){var c=Number(x.credits)||0;return '<span title="'+esc(String(x.day))+": "+c+' cr" style="flex:1;min-width:2px;background:var(--fg);opacity:'+(c?".6":".12")+';height:'+Math.max(2,Math.round(c/max*46))+'px"></span>';}).join("")+"</div>":"";
3182
+ var fails=(d.recentFailures||[]).map(function(f){
3183
+ return '<div style="padding:6px 0;border-top:1px solid var(--border);font-size:12.5px"><b>'+esc(f.tool||"")+"</b> "
3184
+ +'<span style="color:var(--muted)">'+esc(String(f.startedAt||"").slice(0,16).split("T").join(" "))+"</span>"
3185
+ +(f.error?'<div style="color:var(--muted);word-break:break-word">'+esc(f.error)+"</div>":"")+"</div>";
3186
+ }).join("");
3187
+ return '<div class="fade-in">'
3188
+ +'<div style="font-size:15px;font-weight:700">'+(d.scope==="workspace"?"Workspace usage":"Your usage")+"</div>"
3189
+ +'<div style="font-size:12px;color:var(--muted);margin:2px 0 12px">'+esc(from)+" → "+esc(to)+"</div>"
3190
+ +'<div style="display:flex;gap:8px;flex-wrap:wrap">'+tile("Credits",t.credits)+tile("Calls",t.calls)+tile("Failures",t.failures)
3191
+ +(d.scope==="workspace"?tile("Seats",t.seats):"")+"</div>"
3192
+ +table("By tool",["Tool","Calls","Failed","Credits"],(d.byTool||[]).map(function(r){return [r.tool,r.calls,r.failures,r.credits];}))
3193
+ +table("By seat",["Member","Calls","Failed","Credits"],(d.bySeat||[]).map(function(r){return [r.name||r.email||String(r.userId||"").slice(0,8),r.calls,r.failures,r.credits];}))
3194
+ +bars
3195
+ +(fails?'<div class="sec-label" style="margin-top:14px">Recent failures</div>'+fails:"")
3196
+ +"</div>";
3197
+ }
3198
+
3155
3199
  // ─── Vetting strip ───
3156
3200
  //
3157
3201
  // Only show_collab_shortlist sends these fields, so every other creator card
@@ -4850,6 +4894,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4850
4894
  // Credits
4851
4895
  if(d.gettingStarted===true){app.innerHTML=renderGettingStarted(d);setTimeout(reportSize,50);return;}
4852
4896
  if(d.connector==="google_analytics"||d.connector==="search_console"||d.connector==="posthog"){app.innerHTML=renderConnector(d);setTimeout(reportSize,50);return;}
4897
+ if(d.report===true&&d.totals){app.innerHTML=renderUsageReport(d);setTimeout(reportSize,50);return;}
4853
4898
  if(d.balance!=null||d.tier){
4854
4899
  var bal=Number(d.balance)||0,tier=d.tier||"",ff=d.firstFreeTools||[];
4855
4900
  var freeHtml="";
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.46",
4
+ "version": "1.26.48",
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": {