@nooticr/mcp 1.26.42 → 1.26.43

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,10 +52,10 @@ npx @nooticr/mcp login # one-time sign-in (Google)
52
52
 
53
53
  ## Tools
54
54
 
55
- 72 tools, grouped by what you are trying to do. Prices are in nooticr credits and
55
+ 85 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
- Seven of them — the ones under **Answer a question you actually have** — are not
58
+ The job tools under **Answer a question you actually have** are not
59
59
  endpoint wrappers. Each names a job, fans out over the calls that job needs,
60
60
  groups the evidence by whatever you are deciding about, gives every item an id
61
61
  a follow-up tool can act on, and hands the reading to your model rather than to
@@ -191,6 +191,9 @@ Everything above reads someone else's content. These read and generate for **you
191
191
  | `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. |
192
192
  | `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. |
193
193
  | `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. |
194
+ | `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`. |
195
+ | `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. |
196
+ | `get_posthog_analytics` | free | Your PostHog pageview trend, as of the last sync — line a post's date up against traffic to your site. |
194
197
  | `get_content_plan` | free | The saved weekly content plan for a product, if one has been generated. `plan: null` when none has. |
195
198
  | `get_brand_playbook` | free | Your product's brand playbook — name, description and the playbook text — if one has been configured, in the dashboard or by `analyze_product`. Read-only; `available: false` when none exists. |
196
199
  | `review_post` | free | Score a draft before you publish it — hook strength, an A/B hook comparison, aesthetic and storytelling notes, rewritten hooks/captions. Never billed, same as the dashboard's own pre-publish review. |
@@ -21,7 +21,4 @@ export const BACKEND_ONLY_TOOLS = {
21
21
  get_batch_history: "History of the dashboard's compose batches, which only exist once rendering does; stays with #92.",
22
22
  get_latest_analysis: "Returns the dashboard's stored AI fleet-analysis verdict; the public surface sells the evidence, not a judgement of ours.",
23
23
  brand_watch_history: "Not a deliberate exclusion: a candidate for the public surface once it has a trend view to draw. Listed so it reads as pending, not forgotten.",
24
- get_google_analytics: "Site analytics are dashboard-only until #104 decides their shape.",
25
- get_posthog_analytics: "Site analytics are dashboard-only until #104 decides their shape.",
26
- get_search_console_data: "Site analytics are dashboard-only until #104 decides their shape.",
27
24
  };
@@ -444,6 +444,9 @@ export const LOADING_PLANS = {
444
444
  get_scheduled_posts: { label: "Reading your pipeline", kind: "list", n: 5, steps: [], free: true },
445
445
  get_post_performance: { label: "Reading your numbers", kind: "list", n: 5, steps: [], free: true },
446
446
  get_video_stats: { label: "Reading the last sync", kind: "strip", n: 3, steps: [], free: true },
447
+ get_google_analytics: { label: "Reading your last GA4 sync", kind: "text", n: 1, steps: [], free: true },
448
+ get_search_console_data: { label: "Reading your last Search Console sync", kind: "text", n: 1, steps: [], free: true },
449
+ get_posthog_analytics: { label: "Reading your last PostHog sync", kind: "text", n: 1, steps: [], free: true },
447
450
  get_brand_playbook: { label: "Reading the playbook", kind: "text", n: 1, steps: [], free: true },
448
451
  // ─── The product analysis: the one own-account tool that spends ───
449
452
  // The only outbound fetch in this family — it reads an excerpt of the
@@ -1303,6 +1303,45 @@ export const OUTPUT_SCHEMAS = {
1303
1303
  * ui-template.ts's shared posts-gallery view picks this up. Declared here
1304
1304
  * too so a host validating structuredContent does not choke on it.
1305
1305
  */
1306
+ // The connector reads (#104). Each is the backend's connector_sync_cache
1307
+ // row as stored, plus `synced_at`, the hoisted appId/appName, and the
1308
+ // `connector` this surface adds so the view knows which table to draw.
1309
+ get_google_analytics: open({
1310
+ connector: scalar(),
1311
+ propertyId: scalar(),
1312
+ rowsSynced: scalar(),
1313
+ dateRange: open({ from: scalar(), to: scalar() }).nullish(),
1314
+ lastSync: scalar(),
1315
+ synced_at: scalar().describe("When nooticr last synced this. Not live: say this date when quoting it."),
1316
+ message: scalar().describe("Set instead of data when nothing has been synced yet."),
1317
+ appId: scalar(),
1318
+ appName: scalar(),
1319
+ }),
1320
+ get_search_console_data: open({
1321
+ connector: scalar(),
1322
+ clicks: scalar(),
1323
+ impressions: scalar(),
1324
+ topQueries: listOf(z.string()),
1325
+ error: scalar().describe("Search Console's own error at the last sync, when it refused."),
1326
+ synced_at: scalar().describe("When nooticr last synced this. Not live: say this date when quoting it."),
1327
+ message: scalar(),
1328
+ appId: scalar(),
1329
+ appName: scalar(),
1330
+ }),
1331
+ get_posthog_analytics: open({
1332
+ connector: scalar(),
1333
+ projectId: scalar(),
1334
+ result: listOf(open({
1335
+ label: scalar(),
1336
+ data: listOf(z.number()).describe("One count per day, aligned with days."),
1337
+ days: listOf(z.string()),
1338
+ })).describe("PostHog's pageview trend series, as its API returned them."),
1339
+ lastRefresh: scalar(),
1340
+ synced_at: scalar().describe("When nooticr last synced this. Not live: say this date when quoting it."),
1341
+ message: scalar(),
1342
+ appId: scalar(),
1343
+ appName: scalar(),
1344
+ }),
1306
1345
  get_video_stats: open({
1307
1346
  videos: listOf(open({
1308
1347
  id: scalar(),
@@ -338,6 +338,68 @@ export function registerOwnAccountTools(server, makeClient) {
338
338
  return failed("get_brand_playbook failed", err);
339
339
  }
340
340
  });
341
+ /**
342
+ * The three connector reads (#104): GA4, Search Console, PostHog, as of
343
+ * their last sync into nooticr. They are the only tools here that say
344
+ * whether social moved a business metric, which is why they exist; the
345
+ * staleness is why the sync time is said in the text as well as the
346
+ * payload. A model that reads "1,204 clicks" without "as of" states a
347
+ * week-old number as today's.
348
+ */
349
+ const connectorRead = (name, connector, title, description) => server.registerTool(name, {
350
+ title,
351
+ description,
352
+ _meta: viewMeta(name),
353
+ // nooticr's own stored copy: nothing reaches Google or PostHog.
354
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
355
+ inputSchema: z
356
+ .object({
357
+ appId: z
358
+ .number()
359
+ .int()
360
+ .optional()
361
+ .describe("Your product's id. Omit only with a single-app workspace."),
362
+ })
363
+ .strict(),
364
+ outputSchema: OUTPUT_SCHEMAS[name],
365
+ }, async (args, extra) => {
366
+ const client = await makeClient({ ...extra, arguments: args });
367
+ try {
368
+ const proxy = await client.callTool(name, args);
369
+ const data = { ...(proxy.structured ?? {}), connector };
370
+ const synced = String(data.synced_at ?? data.lastSync ?? data.lastRefresh ?? "");
371
+ const lead = data.message
372
+ ? String(data.message)
373
+ : synced
374
+ ? `${title.replace(/^Get /, "")} for ${String(data.appName ?? "your product")}, as of its last sync at ` +
375
+ `${synced} — not live. Say that date whenever you quote a number from it.`
376
+ : `${title.replace(/^Get /, "")} for ${String(data.appName ?? "your product")}; the sync time was not reported, so do not present these as current.`;
377
+ return {
378
+ content: [{ type: "text", text: `${lead}\n\n${JSON.stringify(data, null, 2)}` }],
379
+ structuredContent: data,
380
+ };
381
+ }
382
+ catch (err) {
383
+ return failed(`${name} failed`, err);
384
+ }
385
+ });
386
+ connectorRead("get_google_analytics", "google_analytics", "Get Google Analytics", "Your own product's Google Analytics (GA4) sync, as of its last sync into nooticr — not a live " +
387
+ "GA4 call: the property, the date range synced and how many daily rows came in. The metrics " +
388
+ "themselves (installs, opens, purchases, revenue) feed get_post_performance and growth_brief " +
389
+ "rather than being repeated here. Returns a message rather than an error when nothing has " +
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.");
392
+ 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
+ "search queries — as of its last sync into nooticr, not a live Search Console call. Use it " +
394
+ "to see whether people search for what your social content is about. Returns a message " +
395
+ "rather than an error when nothing has been synced; connect Search Console in API " +
396
+ "Connections first. Takes appId (optional when your workspace has only one product). No " +
397
+ "cost to call.");
398
+ connectorRead("get_posthog_analytics", "posthog", "Get PostHog Analytics", "Your own product's PostHog pageview trend as of its last sync into nooticr — not a live " +
399
+ "PostHog call. Use it to line a post's date up against traffic to your site. Returns a " +
400
+ "message rather than an error when nothing has been synced; connect PostHog in API " +
401
+ "Connections first. Takes appId (optional when your workspace has only one product). No " +
402
+ "cost to call.");
341
403
  server.registerTool("analyze_product", {
342
404
  title: "Analyze Product",
343
405
  description: "Start an AI analysis of your own product: fetches an excerpt of the product's own website " +
@@ -324,6 +324,24 @@ export const TOOL_DEFINITIONS = [
324
324
  description: "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. No cost to call.",
325
325
  inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace."), limit: z.number().int().optional().describe("Max videos (default 20, capped at 50).") }).strict(),
326
326
  },
327
+ {
328
+ name: "get_google_analytics",
329
+ title: "Get Google Analytics",
330
+ description: "Your own product's Google Analytics (GA4) sync, as of its last sync into nooticr — not a live GA4 call: the property, the date range synced and how many daily rows came in. Connect GA4 in API Connections first. No cost to call.",
331
+ inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace.") }).strict(),
332
+ },
333
+ {
334
+ name: "get_search_console_data",
335
+ title: "Get Search Console Data",
336
+ description: "Your own product's Google Search Console clicks, impressions and top queries as of its last sync into nooticr — not a live Search Console call. Connect Search Console in API Connections first. No cost to call.",
337
+ inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace.") }).strict(),
338
+ },
339
+ {
340
+ name: "get_posthog_analytics",
341
+ title: "Get PostHog Analytics",
342
+ description: "Your own product's PostHog pageview trend as of its last sync into nooticr — not a live PostHog call. Connect PostHog in API Connections first. No cost to call.",
343
+ inputSchema: z.object({ appId: z.number().int().optional().describe("Your product's id. Omit only with a single-app workspace.") }).strict(),
344
+ },
327
345
  {
328
346
  name: "get_content_plan",
329
347
  title: "Get Content Plan",
@@ -29,7 +29,7 @@ import { registerCollabTools } from "./collab.js";
29
29
  import { registerAmazonTools } from "./amazon.js";
30
30
  import { registerMarketplaceTools } from "./marketplace.js";
31
31
  /** Current MCP server version — bumped on every deploy for traceability. */
32
- export const MCP_SERVER_VERSION = "1.26.42";
32
+ export const MCP_SERVER_VERSION = "1.26.43";
33
33
  /** MCP Apps extension identifier */
34
34
  const UI_EXTENSION = "io.modelcontextprotocol/ui";
35
35
  /** MIME type for MCP Apps HTML resources */
@@ -642,6 +642,10 @@ export function createMcpServer(rawMakeClient, opts) {
642
642
  "get_scheduled_posts",
643
643
  "get_post_performance",
644
644
  "get_video_stats",
645
+ // The connector reads (#104): stored syncs, drawn by their own view.
646
+ "get_google_analytics",
647
+ "get_search_console_data",
648
+ "get_posthog_analytics",
645
649
  // get_content_plan gets the same card generate_content_plan does: they
646
650
  // return the same `plan` shape, so the generic template already renders it.
647
651
  "get_content_plan",
@@ -48,6 +48,9 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
48
48
  list_watchlist:"Watchlist",
49
49
  detect_spoken_mentions:"Spoken Mentions",
50
50
  nooticr_getting_started:"Getting Started",
51
+ get_google_analytics:"Google Analytics",
52
+ get_search_console_data:"Search Console",
53
+ get_posthog_analytics:"PostHog",
51
54
  compose_sequence:"Compose Sequence",
52
55
  overlay_bake:"Overlay Bake",
53
56
  spawn_variants:"Spawn Variants",
@@ -83,6 +86,9 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
83
86
  list_watchlist:"The creators you are watching, and when you last caught up on each.",
84
87
  detect_spoken_mentions:"Brands named out loud in a video, beside what its caption says.",
85
88
  nooticr_getting_started:"Where your account stands and what to try first.",
89
+ get_google_analytics:"Your GA4 sync, as of its last sync.",
90
+ get_search_console_data:"Search clicks, impressions and top queries, as of the last sync.",
91
+ get_posthog_analytics:"Your PostHog pageview trend, as of the last sync.",
86
92
  compose_sequence:"AI-powered content composition for social posts.",
87
93
  overlay_bake:"Bake text/image overlays onto video or image.",
88
94
  spawn_variants:"Generate multiple content variants from a single seed.",
@@ -3088,6 +3094,62 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
3088
3094
  +'<div class="sec-label">Try next</div>'+steps+"</div>";
3089
3095
  }
3090
3096
 
3097
+ // ─── Connector reads (get_google_analytics, get_search_console_data,
3098
+ // get_posthog_analytics) ───
3099
+ //
3100
+ // Every one is a stored sync, so "as of" leads the card: a number drawn
3101
+ // without its date is read as today's (#104).
3102
+ function renderConnector(d){
3103
+ var names={google_analytics:"Google Analytics",search_console:"Search Console",posthog:"PostHog"};
3104
+ var name=names[d.connector]||"Analytics";
3105
+ var when=String(d.synced_at||d.lastSync||d.lastRefresh||"");
3106
+ var asOf=when?"as of "+esc(when.slice(0,16).split("T").join(" "))+" UTC · not live":"sync time not reported";
3107
+ var head='<div style="margin-bottom:12px"><div style="font-size:15px;font-weight:700">'+esc(name)
3108
+ +(d.appName?" · "+esc(d.appName):"")+"</div>"
3109
+ +'<div data-as-of style="font-size:12px;color:var(--muted);margin-top:2px">'+asOf+"</div></div>";
3110
+ if(d.message){
3111
+ return '<div class="fade-in">'+head+'<div class="empty-state"><div class="icon">📊</div><div class="text">'+esc(d.message)+"</div></div></div>";
3112
+ }
3113
+ function tile(label,val){
3114
+ return '<div style="flex:1;min-width:120px;padding:10px 12px;border:1px solid var(--border);border-radius:10px">'
3115
+ +'<div style="font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.05em">'+esc(label)+"</div>"
3116
+ +'<div style="font-size:18px;font-weight:700;margin-top:3px">'+esc(String(val))+"</div></div>";
3117
+ }
3118
+ var body="";
3119
+ if(d.connector==="search_console"){
3120
+ var clicks=Number(d.clicks)||0,imp=Number(d.impressions)||0;
3121
+ body='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">'
3122
+ +tile("Clicks",fmtNum(clicks))+tile("Impressions",fmtNum(imp))
3123
+ +tile("Click-through",imp?(Math.round(clicks/imp*1000)/10)+"%":"—")+"</div>"
3124
+ +(d.error?'<div style="font-size:12px;color:var(--red);margin-bottom:8px">Search Console refused the last sync: '+esc(d.error)+"</div>":"")
3125
+ +((d.topQueries||[]).length
3126
+ ?'<div class="sec-label">Top queries</div>'+(d.topQueries||[]).map(function(q,i){
3127
+ return '<div data-query style="padding:6px 0;border-top:1px solid var(--border);font-size:13px"><span style="color:var(--muted);margin-right:8px">'+(i+1)+"</span>"+esc(q)+"</div>";
3128
+ }).join("")
3129
+ :"");
3130
+ }else if(d.connector==="google_analytics"){
3131
+ var r=d.dateRange||{};
3132
+ body='<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">'
3133
+ +tile("Days synced",d.rowsSynced==null?"—":d.rowsSynced)
3134
+ +tile("Range",(r.from||"?")+" → "+(r.to||"?"))+"</div>"
3135
+ +(d.propertyId?'<div style="font-size:12px;color:var(--muted)">Property '+esc(d.propertyId)+"</div>":"")
3136
+ +'<div style="font-size:12px;color:var(--muted);margin-top:6px">The metrics themselves feed post performance and the growth brief.</div>';
3137
+ }else{
3138
+ var series=Array.isArray(d.result)?d.result:[];
3139
+ body=series.length?series.map(function(s){
3140
+ var pts=Array.isArray(s.data)?s.data.map(Number):[];
3141
+ var total=pts.reduce(function(a,b){return a+(b||0);},0),max=Math.max.apply(null,pts.concat([1]));
3142
+ var bars=pts.slice(-30).map(function(v){
3143
+ return '<span style="display:inline-block;width:6px;margin-right:2px;background:var(--fg);opacity:.55;height:'+Math.max(2,Math.round((v||0)/max*40))+'px"></span>';
3144
+ }).join("");
3145
+ return '<div data-series style="margin-bottom:12px"><div style="font-size:13px;font-weight:600">'+esc(s.label||s.action&&s.action.name||"Pageviews")
3146
+ +' <span style="color:var(--muted);font-weight:500">· '+fmtNum(total)+" total</span></div>"
3147
+ +'<div style="display:flex;align-items:flex-end;height:44px;margin-top:6px">'+bars+"</div></div>";
3148
+ }).join(""):'<div style="font-size:13px;color:var(--muted)">No trend in the last sync.</div>';
3149
+ }
3150
+ return '<div class="fade-in">'+head+body+"</div>";
3151
+ }
3152
+
3091
3153
  // ─── Vetting strip ───
3092
3154
  //
3093
3155
  // Only show_collab_shortlist sends these fields, so every other creator card
@@ -4768,6 +4830,7 @@ export const NOOTICR_UI_TEMPLATE = `<!DOCTYPE html>
4768
4830
  +wlRows+"</div></div>";return;}
4769
4831
  // Credits
4770
4832
  if(d.gettingStarted===true){app.innerHTML=renderGettingStarted(d);setTimeout(reportSize,50);return;}
4833
+ if(d.connector==="google_analytics"||d.connector==="search_console"||d.connector==="posthog"){app.innerHTML=renderConnector(d);setTimeout(reportSize,50);return;}
4771
4834
  if(d.balance!=null||d.tier){
4772
4835
  var bal=Number(d.balance)||0,tier=d.tier||"",ff=d.firstFreeTools||[];
4773
4836
  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.42",
4
+ "version": "1.26.43",
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": {