Skip to main content
Glama
Nooticr

@orchyn/mcp

Official
README.md
# @nooticr/mcp

MCP (Model Context Protocol) server for [nooticr](https://nooticr.com).

Gives an AI assistant three things: it can **read** real social posts across
ten networks (TikTok, Instagram, YouTube, X, Reddit, LinkedIn, Douyin,
Xiaohongshu, Weibo, Bilibili), **understand** them — transcript, video frames,
comments, the numbers — and **make** something from what it learned: hooks,
variants to film, a scored draft, a repurposed thread.

The understanding is your model's, not ours. Every tool here fetches material
and hands it over with an account of what to do with it; none of them ask a
model of ours for an opinion first. You pay for the fetch and nothing else.

It also **monitors a name**: `search_mentions` sweeps nine of those networks for
every comment that says your brand, inside a date window you choose, and
`search_spoken_mentions` reads the words actually said out loud in TikTok,
YouTube and Douyin videos for the mentions that were never typed anywhere.

Runs over stdio locally or as a hosted connector at `https://mcp.nooticr.com/mcp`.
Billed against your nooticr credits; new accounts get 20 free.

## Install (one link)

**Claude Code** — register the marketplace, then install the plugin:

```
/plugin marketplace add Nooticr/nooticr-mcp
/plugin install nooticr@nooticr
```

**Claude Code / CLI without the plugin**:

```bash
claude mcp add nooticr --user -- npx -y @nooticr/mcp
npx @nooticr/mcp login   # one-time sign-in (Google)
```

**Cursor / any stdio MCP client** (`claude_desktop_config.json`, `.mcp.json`, …):

```json
{
  "mcpServers": {
    "nooticr": {
      "command": "npx",
      "args": ["-y", "@nooticr/mcp"],
      "env": { "NOOTICR_BASE_URL": "https://api.nooticr.com" }
    }
  }
}
```

## Tools

169 tools, grouped by what you are trying to do. Prices are in nooticr credits and
match what the server actually charges.

The job tools under **Answer a question you actually have** are not
endpoint wrappers. Each names a job, fans out over the calls that job needs,
groups the evidence by whatever you are deciding about, gives every item an id
a follow-up tool can act on, and hands the reading to your model rather than to
ours. They fan out, so they cost the sum of what they fetched and every one of
them caps that fan-out with an argument.

### Read a post

| Tool | Credits | What it is for |
|------|---------|----------------|
| `get_social_post` | 1 | The post's facts and media — contentType, title, caption, author, stats, direct media URLs, plus an inline thumbnail. Use when you want the post itself and nothing interpreted. |
| `get_post_transcript` | 1 | The words actually spoken. Where the platform publishes a caption track (TikTok, Douyin, YouTube) it is read as-is — exact rather than inferred, without processing the video; everywhere else, and where a caption track is missing, the post's own audio is transcribed on the same call (posts up to 30 minutes), which needs speech-to-text configured on the server. No transcript, no charge. `format: "srt"` or `"vtt"` also returns a ready caption file built from the track's own timing, at no extra cost. Use before any analysis when the wording matters. |
| `get_post_frames` | 2 | Frames sampled evenly across a post's video, returned as **images you can actually look at** — not a description of them. ffmpeg opens the stream directly rather than downloading it, so HLS works and an expired link is re-resolved on the spot. Verified live at 3/3 on TikTok, YouTube, Instagram, Douyin and X; Reddit works on video posts. A carousel or slideshow returns its own images unchanged. Each frame costs roughly 1,200 tokens of your context. |
| `get_post_comments` | 2 | Top comments plus the themes the platform clusters them into, with which ones the creator pinned or liked. Use when you want to read what people wrote. |

### Understand a post

| Tool | Credits | What it is for |
|------|---------|----------------|
| `analyze_post_text` | 2 | The post's transcript, caption and stats — everything but the pictures. Two fetches: the post itself (1, as `get_social_post`) and `get_post_transcript` (1). Use when the words and numbers answer the question. |
| `analyze_post` | 3 | Frames sampled across the video, as **images your model can actually look at**, plus the transcript. Two fetches: `get_post_frames` (2) and `get_post_transcript` (1). Use when the visuals are the point — framing, editing, on-screen text. |
| `understand_social_post` | 3 | The same two fetches, asked for a description of what physically happens on screen rather than why it works. Use when you need the events, not the strategy. |
| `summarize_post_comments` | 2 | The comment section, every comment with a stable id, and the taxonomy to label them with — sentiment, and whether each is praise, a complaint, a bug report, a question, a request, a comparison or spam. The same `get_post_comments` call, at the same price as reading them directly. Pass `verbatim: true` to have every item rendered whole for quoting rather than clipped to fit; same price. |
| `show_comment_review` | free | Draws the classifications your model produced — every comment with its sentiment and category, filterable and selectable. Makes no requests; it only renders what you pass it. |
| `show_post_analysis` | free | Draws the analysis your model wrote from `analyze_post`/`analyze_post_text`/`understand_social_post` — hook strength, script structure, quotable lines, suggested hashtags and more, whichever fields you produced. Makes no requests; it only renders what you pass it. |
| `compare_social_posts` | 1 | The first of two to five posts, fetched with its stats, and the comparison left to you. Fetch the rest with `get_social_post` at 1 credit each. Use when performance differs and you need to know why. |
| `show_compared_posts` | free | Draws the comparison your model wrote from `compare_social_posts` — each post scored, the winner marked, what differed and the next test worth running. Makes no requests; it only renders what you pass it. |

### Research a niche or a creator

| Tool | Credits | What it is for |
|------|---------|----------------|
| `discover_social_posts` | 2 | Recent posts for a niche across nine networks, with inline thumbnails and `limit`/`offset` pagination. Use to find posts to look at. |
| `get_user_posts` | 2 | One creator's recent posts with stats. Use to scan an account. |
| `search_creators` | 2 | People by what they make — designers, developers, photographers, anyone with an audience — found by craft, niche or keyword. Use when you know the kind of person but not their names. Searches TikTok, Instagram and Xiaohongshu; **not** YouTube, Douyin, X, Reddit or LinkedIn, which cannot be creator-searched upstream. |
| `get_similar_creators` | 2 | Lookalikes for a creator that already works. |
| `suggest_creator_identity` | 2 per network searched, plus 2 to find the handle (6 by default) | The same person on the other networks, from one handle you already know — with the evidence for each: a link published in both bios, one bio naming the other account, an identical handle, a matching display name. **Suggests, never merges**: nothing is stored, a human confirms, and two accounts' follower counts are never added together. TikTok, Instagram and Xiaohongshu. |
| `discover_sounds` | 2 | Trending audio with playable previews. Sound is a major ranking signal on TikTok. |
| `discover_hashtags` | 2 | What to tag with. TikTok comes from the Creative Center trend board — volumes plus rising/cooling/steady. The other eight searchable networks have no trend board upstream, so tags are counted across a live sweep of a niche you name: real and current, but a single sample, so no rising/cooling signal. `source` says which you got. LinkedIn cannot be swept. |
| `find_hook_pattern` | 2 | A creator's recent posts, so their opening lines can be read as a set and turned into fill-in-the-blank templates. One `get_user_posts` call. |
| `search_mentions` | 2 per network (5 for Xiaohongshu) | **Brand monitoring.** Every *comment* that names a term, across nine networks at once, grouped under the post it was left on. A brand is named far more often in the replies than in a caption, so the comment is the unit — not the post. Takes a `since` date to read a past window, and pages with `offset`/`pageSize` so a nine-network sweep does not arrive all at once. Does not read speech inside a video — `search_spoken_mentions` does, on TikTok, YouTube and Douyin. |
| `watch_creator` | free | Add a creator to your watchlist. Stores the handle only — nothing is fetched. |
| `unwatch_creator` | free | Drop a creator from the watchlist. |
| `list_watchlist` | free | Who you are watching: handle, platform, note, and when you last caught up on each — and which saved creator identity each account belongs to, grouped as one person in `byIdentity`. |
| `save_creator_identity` | free | Once the user has confirmed that accounts on different networks are one person (for instance from `suggest_creator_identity`'s evidence), save them as one creator identity: `name`, `accounts` (each `{platform, handle, evidence?}`), `note`. Stores exactly what it is given; an account already in another identity is refused with that identity named, never moved. |
| `list_creator_identities` | free | Every creator identity your workspace has saved, with its accounts and the evidence recorded for each. Where an `identityId` comes from. |
| `update_creator_identity` | free | Rename a saved identity, change or clear its note, or add and remove accounts. An identity keeps at least one account. |
| `delete_creator_identity` | free | Delete a saved identity. Only the grouping goes: the accounts and their watchlist entries stay. |
| `watch_creator_identity` | free | Watch a person rather than one account: every account of a saved identity goes on your watchlist as its own entry with its own baselines, grouped under the identity. Nothing is fetched. |
| `unwatch_creator_identity` | free | Take every account of a saved identity off your watchlist. Accounts watched on their own stay. |
| `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". |
| `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 what the server charges for that work — 2 credits per network (5 for Xiaohongshu) for mentions, a flat 2 credits for competitor, for a report 3 for `niche_report` (a run writes the report it mails, where the `niche_report` tool here hands the posts to your model for 2) or 2 for `discover_social_posts` (plus 1 per post opened for comments), 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. `appIds` ties a watch to products of your workspace (send it on both calls); without it the watch is workspace-wide. |
| `list_brand_watches` | free | Every brand-monitoring watch in your workspace (your teammates' included): term, networks, cadence, cost per run, credits spent so far, runs made, and when the next one is due. `appId` narrows it to one product's watches plus the workspace-wide ones. A competitor watch says which saved creator identity its account belongs to, and `byIdentity` groups the watches on one person. |
| `creative_assets_list` | free | The creative assets (voice-overs, sound effects, music, images, videos, composites) generated in your workspace, newest first, with url, kind, dimensions or duration, and provenance. Filter by `appId`, `kind`, `briefId` or `since`; page with `nextCursor`. |
| `creative_voices_search` | free | Search ElevenLabs' voice library by `query`, `language`, `accent`, `gender`, `age` or `useCase`. Returns a `voiceRef`, name, labels, language, accent and preview URL for each. Library voices only, never a cloned voice. |
| `creative_voiceover` | quoted first | A voice-over of up to 2500 characters with word timings for burned-in captions, in a voice from `creative_voices_search`. The first call returns a quote in credits and charges nothing; call again with `confirm: true` and the `confirmationToken` to generate. Settled to actual usage, never above the quote. |
| `creative_sfx` | quoted first | A short sound effect (0.5-10 s) from a description. Same quote-then-confirm handshake as `creative_voiceover`. |
| `creative_music` | quoted first | An instrumental music bed of 5-60 seconds, generated as a job: confirm the quote, then poll `creative_job_status`. May be switched off on a deployment, in which case it says so. Describe mood, tempo and instruments, not artists or songs. |
| `creative_video` | quoted first | One short video clip (3-30 s, draft quality) from a `prompt`, generated as a job: confirm the quote, then poll `creative_job_status`. Optional `firstFrameAssetId` or up to 3 `referenceAssetIds` (image assets), `aspect` 9:16 or 16:9, `audio` ambient or none. The clip is rendered at the nearest length the video model offers; the quote is for that length and the call settles at it. Generates only; it does not edit an uploaded video. |
| `creative_image` | quoted first | 1-4 still images from a `prompt` with GPT Image (low quality), at a `size` (default portrait 1024x1536), optionally `transparent`. The first call returns a quote in credits and charges nothing; call again with `confirm: true` and the `confirmationToken` to generate. Settled to actual usage, never above the quote. Returns the new `assets` and `creativeCredits`. Keep long text out of the prompt and ask for invented people, not real ones. |
| `creative_image_edit` | quoted first | Edit or combine 1-4 existing image assets (`imageAssetIds`) with a `prompt`, with an optional `maskAssetId`. Same quote-then-confirm handshake as `creative_image`; returns a new asset and `creativeCredits`. |
| `creative_compose` | quoted first | Cut your creative assets (clips, stills, a voice-over, music, sound effects) into one finished 9:16, 4:5 or 1:1 video or a feed still: voice-synced captions in the platform's safe zone, the product's brand colours, music under the voice, an optional end card. Up to 12 segments, 60 seconds and 6 SFX; `draft: true` for a lower-priced preview. One flat render: confirm the quote, then poll `creative_job_status`. Never publishes. |
| `creative_settings_get` | free | Your workspace's creative spend limits (daily, monthly and per-call caps, whether music is on) and what was spent in the last 24 hours and this month. Read-only: an owner or admin changes them in the dashboard. |
| `creative_brief_create` | free | File a creative brief for one product (`brief`, `platforms`, optional `hook`, `audience`, `variants`, `budgetCredits`, `slotAt`). Your Creative Producer employee works the queue, or a teammate picks it up in Creatives. |
| `creative_brief_list` | free | Your creative briefs with their draft, picked and delivered assets. Filter by `status` (oldest first, the order the queue is worked) or `appId`. |
| `creative_brief_update` | free | Move a brief along (`in_progress`, `awaiting_pick`, `delivered`, `failed`, `canceled`) and attach drafts and deliveries; the id lists replace what the brief held. Winners are picked by a person in the dashboard. |
| `creative_job_status` | free | Where an asynchronous creative job stands by `jobId`: queued, running, done, failed or canceled, with the finished assets and the credits quoted, charged and refunded. Poll after `pollAfterSeconds`. |
| `sheets_create` | free | Create a Google spreadsheet in the connected Google account, with named tabs and a header row on each. Returns its id and URL; Nooticr owns the file, so later writes need no approval. Needs the product's Google Sheets connection. |
| `sheets_list` | free | The spreadsheets the connection can see (created by Nooticr or picked by the customer) and whether each may be edited without approval. |
| `sheets_read` | free | Read an A1 range (e.g. `Ledger!A1:F200`) of a spreadsheet; at most 10000 cells. |
| `sheets_append_rows` | free | Append typed rows after the last row of a tab. Free on files Nooticr created or the customer allowed you to edit; any other picked file waits for approval. |
| `sheets_update_range` | free | Overwrite an A1 range with typed values. Same approval rule as `sheets_append_rows`. |
| `sheets_add_sheet` | free | Add an empty tab to a spreadsheet. Same approval rule as `sheets_append_rows`. |
| `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. |
| `set_brand_watch_alerts` | free | Where a watch's findings alert the team, by `watchId`: `email` (the watch's address), `slack` (the Slack the workspace connected in Nooticr's settings) and `minNew`, how many new items a run must find before it alerts. Every run also posts to the watch's thread in the dashboard. Takes no address or URL, so nothing read in a post can redirect a watch. |
| `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. |
| `stop_brand_watch` | free | Stop a watch by `watchId` or `term`. Immediate — the next run does not happen and nothing more is charged. |
| `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. Scheduled as a `create_brand_watch` report, a run is 3 credits, because the run writes the report it mails. |
| `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. |
| `get_wikipedia_pageviews` | free | **Wikipedia attention**: how many people read up to five Wikipedia articles per day or per month, from Wikimedia's official pageviews (bots excluded). Each article's series, total, average, peak and the change between the two halves of the window. Readers of an article, never search volume or demand, and the reply says so. |
| `get_news_volume` | free | How much news coverage a brand, competitor or topic got: articles per day in GDELT's worldwide news index over a window, each entity's share of the coverage against up to two others, and the latest headlines. Counts articles, never readers. |
| `get_search_trends` | free | Google Trends interest for up to five terms on one 0-100 scale, from Google's official Trends API. Off until the server has access to that API (an invite-only alpha); it then refuses with `not_configured` rather than reading the Trends page. |
| `list_watch_mentions` | free | The text your brand watches stored: platform, author, the post's words, its link, its sentiment and when a run saw it, newest first, filterable by watch, date and words. Free because the sweeps were billed when they ran. |
| `check_disclosure` | free | Whether a sponsored caption discloses the partnership the way the creator's market requires (France's "Publicité" or "Collaboration commerciale", Germany's "Werbung", "#ad" elsewhere): present, missing or unclear, the words found and the wording to use. Reads the caption only. |

### Answer a question you actually have

| Tool | Credits | What it is for |
|------|---------|----------------|
| `export_report` | free | A report to paste into a document, built from what the session found: the posts, comments and listings exactly as the tools returned them, as cards with their images, links and figures, between your own analysis. Returns markdown, a standalone HTML document as an embedded resource (`nooticr://report/<id>`), and both again in the structured result. Fetches nothing. |
| `measure_audience_overlap` | 4 + 2 per post opened (16 by default) | Whether two accounts on one network talk to the same people: the commenters under each one's recent posts, the ones under both, and the overlap in each direction (their commenters who already comment under you is a different number from yours under them). Measures engaged commenters on a few posts, not followers, and says so. `postsPerCreator` is the price. |
| `answer_my_audience` | 2 + 2 per post opened (14 by default) | **The mirror of `search_mentions`.** The questions waiting under your *own* posts: recent posts fetched, comments read on each, grouped under the post they were left on, every comment with a stable id, and the ones that read like questions or requests flagged and sorted to the top. It finds and drafts — it cannot post a reply itself: the drafts are for a person to paste in, or to send through `draft_comment_reply` and `post_comment_reply` once the text is confirmed. `limit` caps how many posts are opened, which is the price. |
| `prepare_handoff` | free | **The hand-off to your tracker.** Turns items you classified — a bug report in a comment, a complaint said out loud in a video — into the exact `title`, `body` and `labels` to file through whichever GitHub, Jira or Linear MCP server the host also has connected. nooticr files nothing and holds no tracker credential. The body carries the quote fenced and framed as a third-party report rather than as instructions, because the issue is read next by a coding agent; contact details are redacted, `@handles` and `#numbers` are defanged so filing notifies nobody by accident, and a `searchFirst` string dedupes a second sweep against the first. Makes no requests. |
| `list_comments_to_answer` | free | Recent comments on your *own* connected account that nobody has answered yet, newest first (YouTube once connected with `capabilities: ["manage_comments"]`; other networks come back under `unsupported` with the reason). Read-only. |
| `draft_comment_reply` | free with `text`, 1 per reply nooticr writes | Stores a reply to one comment; nothing is posted. A written reply follows the workspace's brand voice (`appId` picks the product when there are several) and comes with its `rationale`; with no brand voice, product or `guidance` to write from it is refused at no cost. Returns a `draftId` and a `confirmationToken` to show the user first. `commentIds` drafts written replies to up to 10 comments on one account at once, each with its own `draftId` and token, and lists the ones it skipped and why. |
| `post_comment_reply` | free | Posts a stored draft as your own account. Needs the `draftId`, `confirm: true` and the `confirmationToken`, and only after the user has agreed to the exact text. Each comment is answered at most once. |
| `show_audience_replies` | free | Lays your drafts out for a person to work through, grouped under the post, each with what you decided to do about it. Sends nothing; fetches nothing. |
| `show_standings` | free | Draws the standings your model read out of `compare_creators` or `watchlist_standings` — one row per creator, with the window and the median it was scored against. `ranking` records which axis you ordered on, and `tooThin` draws a creator with too short a window as unranked rather than last: missing from a comparison is not the same as losing it. Makes no requests. |
| `show_trend` | free | Draws the trend your model read out of `mention_trend`. `tooShort` marks a series with too few points to call a direction, and `edgeIsRecordStart` marks a left edge that is where the record begins rather than where the conversation did — a chart hiding either is the mistake this view could otherwise make on your behalf. Makes no requests. |
| `track_creator` | 2 | What a creator shipped, and which of it beat **their own** median rather than a raw view count that mostly measures follower count. One post list, whatever the window. If they are on your watchlist it also marks what is new since your last check and moves that marker forward — its own marker, not the one `catch_up_watchlist` keeps. |
| `compare_creators` | 2 per creator (4-10) | Two to five creators side by side on the one axis that compares: each post's ratio to **that creator's own** median. Per creator you get the window that was scored, their median, the share of it that beat that median, how hard they beat it when they did, every ratio, and their highest and lowest post. It does not rank them — "how often they land one" and "how big it is when they do" usually disagree, and a hit rate over a short window is one post either way, so the window travels with every number and the read is yours. |
| `watchlist_standings` | 2 per watched creator | The same, across everyone on your watchlist: "who moved" in one call instead of one per creator. Reading the watchlist is free; the fetches are not, so it confirms the total before spending. `catch_up_watchlist` answers what is **new**; this answers how it **did**. |
| `find_collaborators` | 2, or 4 with a seed | A collaboration shortlist: a keyword search merged with the lookalikes of a creator who already fits, marked by which search found each one. Every candidate also carries the **links pulled out of their bio**, typed and sorted by how much opening one will tell you — a repository to read the code in, their own site, a link hub that holds the real links — so vetting is reading the work rather than re-reading the follower count. Those links are never fetched here: they came out of a field the person being evaluated controls, so the host opens them, and the result says so. It does **not** measure audience overlap — that costs about eight credits a candidate, so the result says so and points at `measure_audience_overlap` for a finalist rather than faking the signal. |
| `find_people_with_problem` | 6 per platform | Find people **describing a problem in their own words** — prospects, early users, anyone whose complaint your product answers. Say the problem the way a person would say it and it searches for posts shaped like that complaint rather than for the topic it is about: searching the topic finds marketing about it, which is what `discover_social_posts` is for. Widens the query into the forms a complaint takes — the plain phrasing, the does-anyone-else question, the is-there-a-tool ask — and marks which one surfaced each post. A broad search and **not** a filter: the posts come back for you to judge, and plenty will be off-target. Defaults to reddit, where people describe workflow pain in sentences. Each platform is searched once per query shape — three by default, at 2 credits a search. Not searchable here: linkedin, which has no keyword post search upstream. Pass `verbatim: true` to have every item rendered whole for quoting rather than clipped to fit; same price. |
| `show_collab_shortlist` | free | Draws the candidates you scored, ranked, and asks the user which to approach. The scores are attributed to your model, not presented as a nooticr rating, and a candidate scored without anything having been opened is marked unverified. Makes no requests. |
| `benchmark_post` | 3 | One post against the creator's own recent distribution, with the post taken back out of its own baseline. Returns median, quartiles, ratio and percentile, so the answer can be "this is an ordinary result, not a failure". Different question from `compare_social_posts`, which weighs two URLs you already picked. |
| `content_ideas_from_audience` | 2 + 2 per post read + 2 (12 by default) | Demand against supply: what your commenters explicitly ask for, set beside what a niche sweep shows is already being made. A gap nobody asked for is noise; a request nobody serves is the opportunity. Falls back to your most-used hashtag when you name no niche. |
| `detect_spoken_mentions` | 2 | **Every brand named out loud in one video.** The spoken transcript and the written caption, side by side, for your model to list each brand with its verbatim sentence, the creator's framing and whether it was said, written or both. Works where the creator never enabled captions, because the audio is transcribed. 1 credit for `get_post_transcript` plus 1 for `get_social_post`; nothing when no transcript can be made. |
| `search_spoken_mentions` | 2 per platform narrowed by niche + 2 per creator handle + 1 per transcript, up to `maxTranscripts` | **The mirror of `search_mentions`, for what was said rather than typed.** Narrows to candidate posts (a niche sweep, named handles, and/or your watchlist), transcribes only the most-viewed survivors up to a hard ceiling, and searches the words for the term — TikTok, YouTube and Douyin, and only where the creator actually enabled captions. Reports how many candidates were found, transcribed and matched, so the spend is legible. |

### Study a market on Amazon

Everything above reads social content. These read a **marketplace** — the place
where the same audience says what it actually bought and why it sent it back.
The question they exist for is a category decision: *we are considering
ashwagandha — what makes people buy, what stops them, what are the incumbents
good at, and where is the gap?* Every one of those answers lives in review text,
so the unit of work is a scan of the category rather than a lookup of a product.

Collection is genuinely slow, and honestly so: each listing is a real browser
render behind Amazon's bot defences, and the pace between them is set by what
Amazon tolerates rather than by what the machine can do. Ten listings with their
reviews is minutes, not seconds — so a scan returns what is ready and hands back
a `scanId` for the rest.

| Tool | Credits | What it is for |
|------|---------|----------------|
| `scan_amazon_category` | 3 per listing collected (30 by default) | The category, collected: listings with prices, star histograms, Amazon's own review-aspect counts and the review text itself, plus the arithmetic over them — price spread, rating spread, which aspects recur across how many brands, and the share of each listing's ratings sitting at 1-2 stars, read from the histogram rather than from the handful of reviews a scrape returns. Pass `query` to search the category, `asins` to pin the competitors a customer named, or both; named competitors keep their place at the front. Because the price is set by an argument, it confirms the total before spending it. |
| `amazon_scan_status` | free | Picks a running collection back up by `scanId` — the listings collected since, with the same rollup. Free: it is the poll `scan_amazon_category` asked for, and billing the second half of one answer would charge for a wait the server chose. |
| `get_amazon_product` | 3 | One listing by ASIN or URL: price, rating, the full star histogram, features, specs, Amazon's review digest and aspect breakdown, and the reviews. |
| `scan_marketplace_category` | 3 per product collected (30 by default) | The same category read on eleven more sites — AliExpress, Cdiscount, Flipkart, Mercado Libre, Lazada, Otto, Rakuten, Trendyol, Jumia, Vinted and Temu. Vinted is resale rather than retail, so its `rating` describes the seller and the scan says so in `ratingScope` — do not read it as a product's reviews. `marketplace` picks the site; `market` picks the storefront for the sites that have several, and the argument names the real set per site so a model cannot send Otto a Lazada region. Same rollup, same confirmation before spending. |
| `marketplace_scan_status` | free | Picks a running marketplace collection back up by `scanId` and the same `marketplace`. Free, for the same reason `amazon_scan_status` is. |
| `get_price_history` | free | One product's `price` over time, or its `rating`, `reviewCount` or `availability` with `metric`, from the observations stored each time it was scanned. `marketplace` and `id` name it as `get_marketplace_product` does; `from`/`to` default to the last 30 days. Returns the points oldest first with min, max, first, last and the change from first to last. Reads stored data only; a product never scanned answers `recorded: false`. |
| `marketplace_changes` | free | What moved in a category since the previous scan of the same `marketplace` and `query`: products that entered or left the results and ones whose price or place changed. Read from the two latest stored scans, so it fetches nothing; a category scanned once says it has no earlier run. |
| `track_price` | free | Follows a listing already read here (by `marketplace` and `id`, as `get_marketplace_product` names it) on the workspace's Price history page. `competitorOf` draws it against one of the workspace's own tracked listings; `productLine` groups listings. Tracking again changes only the fields given. Fetches nothing, so a product never read is refused with the read that records it. |
| `untrack_price` | free | Stops following a tracked listing. One the workspace's own scans still return stays on the page as seen in a scan; one that was only tracked is removed. A listing that was not tracked is a no-op. |
| `get_marketplace_product` | 3 | One product from any of the twelve sites, by id or URL: price, rating, the full star histogram, specs and the reviews. |
| `show_amazon_category_insights` | free | Draws the read **your model** wrote — purchase drivers, barriers, what each brand does well, the gaps, the positioning angles — beside the listings and their reviews, so a person can click a product and check any claim against the text it came from. The conclusions are attributed to your model, never presented as a nooticr rating of anyone's product. Given the `scanId`, it re-reads the listings from the scan itself (free) instead of drawing whatever the model re-sends, and flags any ASIN the read cites that the scan does not contain; without one, the card says its listings were not checked. |
| `show_marketplace_category_insights` | free | The same card for the other eleven marketplaces: your read beside that site's listings and reviews, labelled with the right site, re-read from the scan by `scanId` and `marketplace` so nothing is re-typed. |

### Make something

| Tool | Credits | What it is for |
|------|---------|----------------|
| `generate_hook_ideas` | 2, or free | The source post and its transcript, to write openings against. Give a topic instead of a url and it fetches nothing and costs nothing. |
| `show_hooks` | free | Draws the hooks your model wrote from `generate_hook_ideas` — each one with the device it uses and who it stops. Makes no requests; it only renders what you pass it. |
| `evaluate_social_draft` | free | **Your** draft back with the rubric to hold it to — hook, clarity, payoff, specificity and fit, each scored 1-10, plus the three fixes worth making and a rewritten opening. Fetches nothing: the text is already yours. The only tool that runs before the content exists. |
| `adapt_post_for_platform` | 2 | The source post and its transcript, to rewrite for other surfaces — X thread, LinkedIn post, carousel slides, YouTube metadata, newsletter. |
| `show_repurposed_post` | free | Draws the rewritten copy your model produced from `adapt_post_for_platform` — one entry per surface. Makes no requests; it only renders what you pass it. |
| `generate_post_variants` | 2 | The post that worked, with its transcript, to build variants from: hook, the angle that changes, ordered shot beats and a CTA. |
| `show_variants` | free | Draws the variants your model wrote from `generate_post_variants` — each one's hook, angle, beats and CTA. Makes no requests; it only renders what you pass it. |

### Account

| Tool | Credits | What it is for |
|------|---------|----------------|
| `nooticr_getting_started` | free | Where this account stands — balance, connected accounts, watchlist — and suggested next calls, each with its price and an example. The server's `instructions` point every host at it. |
| `check_nooticr_credits` | free | Balance and billing URL. |
| `nooticr_settings_read` | free | Read your default social platform and result count. |
| `nooticr_settings_update` | free | Save the default platform and result count used by supported searches when arguments are omitted. |
| `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. |
| `get_tool_run` | free | One run from that history by id, with its full error. |
| `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. |
| `nooticr_login` | free | Re-link the account when a call fails with an authentication error. |
| `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**. "Publish" is the grant an AI employee's posts use: no tool here publishes a post. A post goes out when you approve that employee's `schedule_post` action in the Nooticr dashboard's Inbox, or on its own if you turned on autopilot for publishing for that employee. "Manage comments" reports the platform's grant; this tool posts nothing; a reply goes out only through `post_comment_reply`, on a draft the user confirmed. 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. |
| `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. For YouTube, `capabilities: ["manage_comments"]` also asks for the grant `list_comments_to_answer` and `post_comment_reply` need. |
| `hire_employee` | free to call | Hire an AI employee (for example a Social Media Manager) that works on a schedule inside your workspace. Two calls by design: the first creates nothing and returns a quote and a `confirmationToken`; call again with `confirm: true` and that token only after you say yes. Every employee is a monthly subscription to its role's plan (`level` and `currency` pick it), never charged in credits; the quote names the price and a `hireId`: the confirm sends it back unchanged, since the token is bound to it. Public actions wait for your approval unless autopilot is on. |
| `list_employees` | free | Your AI employees with status, schedule, next run and how much of its plan's included work is used this month; `includeFired: true` adds the ones let go. |
| `pause_employee` | free | Pause an AI employee (it keeps its memory and thread) or resume it with `resume: true`. Only the person who hired it or a workspace admin can. |
| `fire_employee` | free | Let an AI employee go for good: runs stop and actions awaiting approval are discarded. The first call only describes what will happen; confirm with `confirm: true`. |
| `list_teams` | free | Your AI employee teams: status (and why one is paused), members, approvals waiting, open hand-off tasks and the share of included work used. Where a `teamId` comes from. |
| `get_team_status` | free | One team in detail: its members with level, state and payment state, this week's plan, the brief's version and what it is missing, and whether you may change it. |
| `list_team_templates` | free | The teams your workspace can build: their roles, whether each can start now and what it needs, and each level's monthly price. With a `goal`, the team the Build-my-team page suggests for it. Hires nothing. |
| `get_team_draft` | free | Your team draft, the same one the Build-my-team page edits: answers so far, the step, what a launch still needs, its page link and its price as it stands. |
| `update_team_draft` | free | Saves answers, the chosen team or the step to that draft (a JSON merge patch). Launches and charges nothing. |
| `hire_team` | free to call | Launches a team. Two calls by design: the first returns a quote (members, levels, prices, the total, what waits for approval) and a `confirmationToken`; the confirmed call creates the team waiting for payment, which you finish on the Teams page of your Nooticr dashboard. Nobody starts before that. |
| `update_team_brief` | free | Changes what a team works from: the patch is stored as the brief's next version, and the reply says which member picks it up when. |
| `pause_team` | free | Pauses a team (its runs stop; the subscription does not) or resumes what the team paused. |
| `fire_team` | free | Lets a team go. The first call only says what will happen; confirm with `confirm: true`. Nothing is refunded. |
| `crm_upsert_creator` | free | Add a creator to your creator pipeline, or update the one already there (matched by `platform` + `handle`). Only the fields you pass change; new creators start as `prospect`. Every number cited goes in `evidence`. |
| `crm_list_creators` | free | Your creator pipeline, most recently updated first, each with its last interaction. Filter by `status`, `appId`, `query` or `dueBefore`. |
| `crm_update_status` | free | Move a creator to a new pipeline status and record why. `do_not_contact` stays unless a person changes it; AI employees may only use `prospect`, `shortlisted` or `paused`. |
| `crm_log_interaction` | free | Record a note, vetting result, outreach draft, reply or deliverable against a creator. AI employees cannot log `outreach_sent`. |
| `list_pending_actions` | free | What your AI employees are waiting to do until you approve, each with a preview of what will be published, sent or changed. Optionally one `employeeId`. |
| `get_employee_audit` | free | One AI employee's audit trail, newest first: runs, model calls and their cost, tool calls (arguments redacted), sources read, approvals with the approver's edits, executed actions and their external ids, config changes, pauses and errors. Filter by `from`/`to`, `types`, `runId` or `actionId`; `verify: true` re-checks the tamper-evident hash chain. Workspace owners and admins only. |
| `approve_action` | free | Approve one pending action by `actionId`. The first call returns the preview and a `confirmationToken` and runs nothing; after you confirm, the stored action runs exactly once. |
| `reject_action` | free | Reject one pending action, with an optional note the employee can learn from. |
| `connect_store` | free | A link a workspace owner or admin opens to connect an Amazon seller account, a Shopify shop or a WooCommerce store. They approve at the store; nothing is connected until they do, and no credential passes through the tool. A PrestaShop store connects with a key, so it is connected in the dashboard's Settings, never through chat. |
| `list_store_products` | free | Read the connected store's own catalogue (Amazon, Shopify, WooCommerce or PrestaShop): title, description, status, price, and Amazon listing issues. Read-only. |
| `get_sales_and_traffic` | free | Your connected Amazon store's Sales and Traffic report for up to 90 whole days: sessions, page views, conversion (unit session %), Buy Box share, units and sales, in total, per day and per product. Amazon builds it as a report, so a first call can answer `in_progress` with the call to make a minute later. |
| `get_account_health` | free | Your Amazon seller account's health rating and each performance rate (order defects, late shipments, cancellations, valid tracking) against Amazon's own target, with the ones breached. |
| `get_fba_inventory` | free | Your FBA stock per SKU: fulfillable, inbound, reserved, unfulfillable and total units. Read-only. |
| `list_connected_reviews` | free | Reviews of your business on the review platforms you connected with your own account (Trustpilot, Judge.me, Yotpo, Okendo, Google Business Profile), with the star histogram, the average and the 1-2 star share. A platform you have not connected refuses with `not_connected`. |
| `draft_review_reply` | free | Stores a public reply to one Trustpilot or Google Business Profile review for you to approve; the reply guards run first (no link, contact detail or remedy your written policy does not name). Returns a `draftId` and a `confirmationToken`. Posts nothing. |
| `discord_list_channels` | free | The text channels of the Discord server you connected. |
| `discord_read_messages` | free | Recent messages in one channel of that server, newest first. When the bot lacks Discord's message content access it says the channel is not readable, rather than quiet. |
| `draft_discord_reply` | free | Stores a reply to one Discord message for you to approve. Returns a `draftId` and a `confirmationToken`. Posts nothing. |
| `post_drafted_reply` | free | Posts a stored review or Discord reply publicly, as the business, once you have agreed to its exact text: `draftId`, `confirm: true` and the `confirmationToken`. Each review or message is answered once. |
| `create_product` | free | Create a product in your own workspace — the row every other own-account tool needs before it has anything to work with. Always creates in the calling session's workspace, never one you could name. `name` and `slug` are required; every other field is snake_case and read by exact key, so a camelCase `websiteUrl` lands nowhere. Writes no brand playbook. |
| `update_product` | free | Patch a product's fields. An omitted argument leaves its column unchanged, so this cannot blank a field by not mentioning it, and the result lists what was actually written — a misspelled field shows up as one that did not change rather than as a silent no-op. |

### Your own product

Everything above reads someone else's content. These read and generate for **your own** — a different product, a different balance (your workspace's plan AI credits, not the personal MCP credits above), billed exactly like the matching dashboard button.

| Tool | Credits | What it is for |
|------|---------|----------------|
| `list_own_apps` | free | Every product in your workspace — call this first when you have more than one and another tool below asks for `appId`. |
| `create_product` | free | Create a new product in your workspace — `name`/`slug` required, plus optional `description`, `website_url`, `niche`, `product_type` and store-listing fields (snake_case — see note below). Takes no workspace id; always creates in your own. Does not generate a brand playbook — call `analyze_product` for that. |
| `update_product` | free | Patch a product's fields — omitted arguments leave their column unchanged. Same snake_case field names as `create_product`. |
| `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. |
| `get_post_performance` | free | Your own already-published posts with their engagement counters — views, likes, comments, shares, saves, impressions, format, platform, post date — and `engagementPerView`, (likes + comments + shares + saves) ÷ views, the rate to rank on rather than raw views. No reach, watch time or follower counts. Pair with `growth_brief` for an interpretation. |
| `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. |
| `get_google_analytics_data` | free | Your product's GA4 sync **as of its last sync into nooticr, not live**: property, date range, rows synced, sessions, users, key events and revenue per default channel group, and sessions and key events per week over about 13 months. |
| `get_search_console_data` | free | Search Console as of the last sync: the site's clicks, impressions, click-through rate and average position over the window (a site total, not a sum of query rows), up to 50 queries with their own figures, and clicks and impressions per week over about 16 months. A figure the sync could not read is absent, never zero. |
| `get_search_performance` | free | Your product's own site in Google Search, **live** from its connected Search Console: clicks, impressions, CTR and position by query, page, country, device or date, with `filters` and `compare_to_previous`. Snake_case arguments, `app_id` required. `site` reads another Search Console property of the same site (a domain property beside a URL-prefix one) when the connected account can open it; it also applies to `inspect_url` and `get_sitemaps`. |
| `inspect_url` | free | Whether Google indexed pages of your own site: verdict, coverage, Google's canonical against yours, last crawl, robots and fetch state. Only URLs on the connected site; at most 20 per call and 20 per agent run (each spends the site's own daily inspection quota). |
| `get_sitemaps` | free | The sitemaps submitted for your own site, with submitted and indexed counts, errors and warnings; `url` for one sitemap's detail. Read-only: it cannot submit one. |
| `get_page_speed` | free | Core Web Vitals for a page of your own site: real-user field data (LCP, INP, CLS) when there is enough, plus lab scores and the biggest savings. `strategy` is `mobile` (default) or `desktop`. |
| `get_posthog_analytics` | free | Your PostHog pageview trend, as of the last sync — line a post's date up against traffic to your site. |
| `list_app_reviews` | free | Your app's App Store and Google Play reviews as the daily sync stored them: stars, text, territory, app version and whether you replied, with the star histogram and the store's own displayed rating beside it. |
| `get_tracked_conversions` | free | Counts of the UTM-tagged events your own site or app sent to Nooticr (link clicks, sign-ups, purchases, installs) per campaign, source or medium over up to 90 days. Counts only, no person's data. |
| `get_content_plan` | free | The saved weekly content plan for a product, if one has been generated. `plan: null` when none has. |
| `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. |
| `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. |
| `draft_post` | plan AI credits | A ready-to-use draft (title, caption, hashtags, per-slide script) for a topic, grounded in your product's name. Returns text only — saves nothing. |
| `growth_brief` | plan AI credits | A plain-language brief — the one insight that matters, wins, risks, next actions — grounded in your real post history and synced analytics. |
| `generate_content_plan` | plan AI credits | A one-week, day-by-day content plan for your creators, grounded in what already worked. Saved — fetch it later with `get_content_plan`. |
| `generate_captions` | plan AI credits | Timed on-screen caption cues for a video — a transcript plus start/end-timed lines. Returns cue data only. |
| `analyze_product` | plan AI credits | Start an AI analysis of your product: fetches an excerpt of its own website (a real outbound fetch, not just nooticr's own data), reads its posts and fleet performance, and writes the result as its brand playbook. Runs in the background — returns a `jobId`, poll it with `analyze_product_status`. 10 plan AI credits, first analysis free per workspace. |
| `analyze_product_status` | free | Poll a job `analyze_product` started. Returns `state` (pending/thinking/done/error) and, once done, the analysis. Free — the cost was already charged when the job started. |
| `list_social_connections` | free | The social accounts you've connected and what each is allowed to do — read, publish, manage comments. Publishing happens through an AI employee, when you approve its post in the dashboard, or on its own if you turned on autopilot for publishing for that employee; not from here. Also lists which platforms can be connected at all. |
| `connect_social_account` | free | A link to open to connect one account. You approve at the provider; nothing connects until you do, and no credential ever passes through this tool. `capabilities: ["manage_comments"]` asks YouTube for the comment-management grant. |

### How billing works

- New accounts get **20 free credits**.
- **Every tool is priced at what it fetches upstream**, and bills from the first
  call. A tool that fans out to two fetches costs both — `analyze_post` is 2 for
  the frames plus 1 for the transcript — and its description says so. There is
  no free first use: that grant belonged to the AI calls, and there are none.
- A call that fails is **refunded automatically**, and a call interrupted
  mid-flight is billed **once at most** — retries are idempotent.
- Platform admins bypass credit debiting entirely.

## Interactive cards in Claude / ChatGPT chat

Every tool that returns posts also renders **inline interactive cards** directly
in the chat (MCP Apps `ui://nooticr/view` resource rendered in a sandboxed
iframe):

- **Video posts** (TikTok/IG/YouTube/Douyin/LinkedIn) — an inline `<video>`
  player with the thumbnail as poster, playing the re-hosted permanent MP4
  (no expiring CDN tokens).
- **Carousels / slideshows** — a horizontally scrollable strip of every slide
  with an image count chip.
- **Single images** — inline thumbnail.
- **Text-only posts** (LinkedIn / X) — a styled quote block of the post text.
- **Official brand marks** — each card shows the platform's real logo
  (simple-icons) in its brand color instead of an emoji.

`search_mentions` renders a different view, because monitoring is triage rather
than browsing:

- **Each row is a person saying something.** Their picture, with the network's
  mark on it, then the handle and when they wrote it — "today at 7:25 PM", not
  a timestamp — then the comment with the term highlighted everywhere it
  appears, then what it earned in likes and replies. A `×N` badge marks a
  comment that names the brand more than once.
- **Each post carries its reach.** The same sentence under a 25K-upvote thread
  and under a post nobody saw are not the same problem, and nothing else on
  screen tells you which one you are reading.
- **The per-network counts are filters.** Click one to narrow to it; a network
  that answered with nothing is shown but is not clickable, because filtering to
  it is a dead end. Filtering and sorting redraw from what you already paid for
  — the view never re-queries.
- **A burst collapses.** One post with a run of near-identical replies (a
  coordinated fan campaign, say) shows the first few and offers the rest, so it
  cannot push four other networks off the screen.
- **Comments are selectable, and the selection is agentic.** Tick any comments,
  or select a whole thread at once, and send them — the view hands the host the
  comment **ids the tool issued**, so the model can reply to, escalate or
  analyse exactly those. Selections survive filtering and sorting.
- **Load more** pages from the `nextOffset` the tool returned.

Also **inline thumbnails** render for the model / plain-text clients:

- `get_social_post` / `understand_social_post` / `analyze_post` — up to 4 frames inline (poster + carousel slides). The full `mediaItems[].preview_url` + `thumbnailUrl` stay in `structuredContent` for the model to reason over.
- `discover_social_posts` / `get_user_posts` / `analyze_creator_profile` — each returned post shows its thumbnail inline (up to 4 at once) together with its title/caption + views/likes/comments. Say **"next"** or **"show more"** — Claude will re-call with `offset`/`limit` pagination. Say **"analyze the 2nd one"** — Claude calls `analyze_post` or `understand_social_post` on that URL.
- **Batch analysis** — ask "analyze all 4" or "understand these 3 in batch" and Claude will call `analyze_post`/`understand_social_post` once per URL in parallel and summarize. For large batches, `discover_social_posts` + a follow-up `analyze_post` per URL covers it.

## Your own model does the thinking

There is no other mode. Every tool returns **the material an analysis would have
been built from**, at the price of the fetch, and asks your model to reason over
it. Nothing here calls a model of ours, so nothing here sells you a judgement.

For the visual tools that means **actual frames** — real image content blocks,
not a description of them — paired with the transcript. Measured on Claude Code:
~1,200 tokens per 1280×720 frame, eight frames read back correctly and in order.
Twenty frames is about 2.4% of a million-token context.

| Tool | What comes back | Credits |
|---|---|---|
| `analyze_post` | frames as images, plus the transcript and stats | **3** (2 + 1) |
| `summarize_post_comments` | the comments, each with an id, and the labels to use | **2** |
| `analyze_creator_profile` | the creator's recent posts and their numbers | **2** |
| `evaluate_social_draft` | your own draft, and the rubric to score it against | **free** |

The prices above are derived from the fetches each tool makes rather than
written down twice — see `EVIDENCE_PLANS` and `planCost` in
`src/shared/evidence.ts`. `evaluate_social_draft` has no plan at all: it reviews text you
already have, so there is nothing to fetch and nothing to charge for.

`show_comment_review` closes the loop: hand back your classifications and it
draws them — every comment with its sentiment and category, filterable and
selectable. Free, and it makes no requests.

## Before an expensive call, it asks

Most tools print their price in their own description, so a call costs what you
already read. Six do not, because their price is set by an argument — or by a
list the request never mentions:

- `search_mentions` bills **per network swept**, so a bare "monitor my brand"
  sweeps all nine for 21 credits.
- `catch_up_watchlist` bills **per creator**, so the price is the length of your
  watchlist.
- `answer_my_audience`, `content_ideas_from_audience` and `measure_audience_overlap`
  bill **per post opened** (14, 12 and 16 credits at their defaults), so they ask
  once the real post count is known rather than on a worst case.
- `search_spoken_mentions` bills per network, per handle and per transcript, so
  it asks on the worst case it could reach.
- `create_brand_watch` asks **however small the number is**, because what is
  being agreed to is a recurring charge rather than one run.

Above 6 credits those first five ask first, over MCP `elicitation` — the client
shows the number and you accept or decline. Declining spends nothing and is not
an error. A client that does not support elicitation is not blocked; the call
runs as it always did.

Two rules go further than the threshold. A scheduled watch always asks, because
the standing arrangement is the thing being approved. And a watch that emails a
digest refuses to accept a `deliverTo` address unless a person actually saw the
dialog and said yes — "nobody could be asked" is a fine answer for a spend and
the wrong one for an address.

## Prerequisites

- Node.js >= 18 (tested on Node 22)
- An nooticr account (created automatically on first sign-in — Google sign-in
  via `npx @nooticr/mcp login`; the dashboard is **not** required: the server
  auto-creates a default workspace + app for new accounts)
- Access to an nooticr server — defaults to the cloud API
  (`https://api.nooticr.com`); point `NOOTICR_BASE_URL` at your own deployment
  for local development

## Quick start

```bash
# 1. Sign in with your nooticr account (Google sign-in opens in your browser)
npx @nooticr/mcp login
#    or with email/password:
npx @nooticr/mcp login --email you@example.com --password '...'

# 2. Add it to your MCP client (see install section above) — or run it manually:
npx @nooticr/mcp            # stdio (default; for Claude Desktop / Cursor)
npx @nooticr/mcp --http     # remote HTTP with OAuth (for OpenAI Agents SDK)
```

`login` stores your nooticr tokens in `~/.config/nooticr-mcp/credentials.json`
(mode `0600`). If your browser cannot be opened automatically, copy the URL it
prints into a browser manually.

## Running on a server (no browser)

`login` and the OAuth flow both need a browser once, and both leave behind a
15-minute access token kept alive by a refresh token. That is the wrong shape
for an integration running server-side: nothing there can complete a consent
screen, and a refresh chain that breaks has to be repaired by hand.

An **API key** is the other way in. Mint one from a browser at
[mcp.nooticr.com/dashboard](https://mcp.nooticr.com/dashboard) — sign in, name
the key, copy it — or from a machine you *can* sign in on, once:

```bash
npx @nooticr/mcp login                                  # the one interactive step
npx @nooticr/mcp api-key create --name "aybee-prod"     # prints the key, once
```

Then give it to the deployment. There is nothing else to configure and nothing
to refresh — the key stays valid until you revoke it.

```bash
# stdio, as a subprocess of your own service
NOOTICR_API_KEY=nk_... npx @nooticr/mcp
```

On a server, prefer the environment variable: it is what every container
runtime and secret manager already injects, and it leaves nothing on a disk
that a redeploy is going to throw away. Where setting one is the awkward part
— a laptop, or a client like Claude Desktop that does not inherit your shell —
store the key in the credentials file instead:

```bash
npx @nooticr/mcp login --api-key nk_...
```

That checks the key against the server before writing it, so a typo fails
while you are still there to fix it, and then behaves exactly like the
environment variable: no browser, no refresh, valid until revoked. The file
holds one credential, so this replaces any session an earlier `login` left
behind (and `NOOTICR_API_KEY` still overrides it).

```http
# or against the remote endpoint, as an ordinary bearer token —
# no /authorize, no PKCE, no redirect URI
POST https://mcp.nooticr.com/mcp
Authorization: Bearer nk_...
```

Managing them, from the dashboard's **API keys** card or the CLI:

```bash
npx @nooticr/mcp api-key create --name "staging" --expires-in-days 90
npx @nooticr/mcp api-key list
npx @nooticr/mcp api-key revoke <id>
```

Every command takes `--json` for scripting. A few things worth knowing:

- The key is shown **once**, by `api-key create`. nooticr stores only a hash of
  it and cannot show it again; `list` shows the head (`nk_1a2b3c4d…`) so you
  can tell keys apart.
- A key carries the scope of whoever created it — their account, and the
  workspace they were in (`--workspace-id` picks another one you belong to).
- A key **cannot create or revoke keys**. That is deliberate: it means a leaked
  key cannot mint a replacement that outlives revoking the original.
- `--expires-in-days` is optional. Without it a key does not expire on its own.
- Revoking takes effect immediately at the API. The hosted remote endpoint
  caches the "this key is real" answer for up to five minutes, which only ever
  lets a revoked key see the tool list — every call that touches your account
  is checked against nooticr-server on the spot.

## Usage in Claude Desktop

After `npx @nooticr/mcp login`, add to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "nooticr": {
      "command": "npx",
      "args": ["-y", "@nooticr/mcp"]
    }
  }
}
```

Tokens are resolved from the credentials file written by `login` (or from
`NOOTICR_ACCESS_TOKEN`). If your client does not inherit your shell environment,
set the env vars explicitly:

```json
{
  "mcpServers": {
    "nooticr": {
      "command": "npx",
      "args": ["-y", "@nooticr/mcp"],
      "env": {
        "NOOTICR_BASE_URL": "http://localhost:8080"
      }
    }
  }
}
```

## Usage in Cursor

In `.mcp.json` (project root) or `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "nooticr": {
      "command": "npx",
      "args": ["-y", "@nooticr/mcp"]
    }
  }
}
```

After adding the server, run `npx @nooticr/mcp login` in your terminal — Cursor
spawns the server with your environment, so it picks up the stored credentials.

For remote/HTTP usage, Cursor can connect to the OAuth-enabled HTTP mode with
`npx @nooticr/mcp --http` running, pointing the server URL at
`http://localhost:3457/mcp`. Remote clients (including Cursor) discover the
OAuth endpoints from
`http://localhost:3457/.well-known/oauth-authorization-server`, open the
"Sign in with Google" page, and store the resulting access token.

## Usage with OpenAI Agents SDK

Start the HTTP transport:

```bash
npx @nooticr/mcp --http --port 3457
```

Python:

```python
from agents import Agent, Runner
from agents.mcp import RemoteMCPClient

async def main():
    async with RemoteMCPClient(
        url="http://localhost:3457/mcp",
        auth_provider="oidc",  # OAuth flow opens your browser once
    ) as client:
        agent = Agent(name="nooticr", mcp_servers=[client])
        result = await Runner.run(
            agent,
            "Analyze this video: https://www.youtube.com/watch?v=dQw4w9WgXcQ",
        )
        print(result.final_output)
```

TypeScript (Agents SDK):

```ts
import { Agent } from "agents";
import { RemoteMCPClient } from "agents/mcp/client";

const client = new RemoteMCPClient({
  url: "http://localhost:3457/mcp",
  authProvider: "oidc", // opens the browser for the OAuth flow
});

const agent = new Agent({
  name: "nooticr",
  mcpServers: [client],
});

const result = await agent.run(
  "Analyze this video: https://vm.tiktok.com/abc123/",
);
console.log(result.output);
```

For a local stdio process with the Agents SDK, use `StdioMCPClient` (Python:
`StdioMCPClient(command="npx", args=["@nooticr/mcp"])`).

## Command line

```
nooticr-mcp                    Start in stdio mode (default transport)
nooticr-mcp --stdio            Same as above
nooticr-mcp --http [--port N]  Start the remote HTTP transport with OAuth (default port 3457)
nooticr-mcp login              Sign in to nooticr via Google in your browser
nooticr-mcp login --email ... --password ...   Password login
nooticr-mcp login --api-key nk_...             Sign in with an API key, no browser
nooticr-mcp api-key create [--name N] [--expires-in-days D] [--workspace-id W] [--json]
                               Mint a key for a server with no browser
nooticr-mcp api-key list [--json]         List this account's keys
nooticr-mcp api-key revoke <id> [--json]  Revoke one
nooticr-mcp --help             Show help
```

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `NOOTICR_BASE_URL` | `https://api.nooticr.com` | nooticr server base URL (trailing slash stripped) |
| `NOOTICR_ACCESS_TOKEN` | — | nooticr JWT access token; takes priority over everything below |
| `NOOTICR_API_KEY` | — | nooticr API key from `api-key create` — no browser, no refresh, valid until revoked; takes priority over the credentials file (including a key `login --api-key` put there) |
| `NOOTICR_CREDENTIALS_FILE` | `~/.config/nooticr-mcp/credentials.json` | token store path |
| `NOOTICR_PUBLIC_URL` | `http://localhost:3457` | public base URL advertised in OAuth metadata (HTTP mode) |
| `NOOTICR_PORT` | `3457` | port for `--http` and `login` |
| `NOOTICR_TRANSPORT` | `stdio` | `stdio` or `http` (same as `--http`) |

## How authentication works

**stdio mode** (Claude Desktop, Cursor): the server uses the first credential
it finds — `NOOTICR_ACCESS_TOKEN`, then `NOOTICR_API_KEY`, then the credentials
file written by `login`. That file holds one credential, whichever `login` last
wrote: an API key (`login --api-key`) or a session. A session token that has
expired is refreshed with the stored refresh token, and a `401` from the
nooticr API is retried once after a refresh. An API key is never refreshed
wherever it came from: it does not expire, so a `401` on one means revoked or
mistyped, and retrying would only hide that.

**API key** (any transport, no browser): send it as the bearer token, or set
`NOOTICR_API_KEY`. nooticr-server resolves the key to its owner and the
workspace it was scoped to, and every authorization check downstream sees
exactly what it would have seen for a signed-in session. See
[Running on a server](#running-on-a-server-no-browser).

**HTTP mode** (OpenAI Agents SDK, remote clients): the server runs its own
OAuth 2.0 authorization server (Authorization Code + PKCE S256, public client,
per the MCP 2025-03-26 spec):

- `GET /.well-known/oauth-authorization-server` — metadata
- `GET /authorize` — validates the request (loopback or https redirect URIs)
  and forwards the browser to nooticr's Google sign-in
- `GET /oauth/callback` — our own loopback callback; exchanges nooticr's
  completion code for nooticr JWTs and redirects back to the MCP client with a
  one-time code
- `POST /token` — verifies PKCE and issues an opaque Bearer token bound to the
  nooticr session (valid 1 hour)
- every MCP RPC validates the Bearer token against the session map — or, when
  the bearer is an API key, forwards it to nooticr-server as the caller's own
  credential, which is what makes this endpoint usable with no browser at all

## Supported URLs

- TikTok: `tiktok.com/*`, `vm.tiktok.com/*` (and `www.`/`m.` subdomains)
- Instagram: `instagram.com/*` (reels, posts, carousels), `instagr.am/*`
- YouTube: `youtube.com/*` (including `/shorts/`), `youtu.be/*`, `m.youtube.com/*`
- X: `x.com/*`, `twitter.com/*`
- Reddit: `reddit.com/*`, `redd.it/*`
- Weibo: `weibo.com/*`, `weibo.cn/*`
- Douyin: `douyin.com/*`
- Xiaohongshu: `xiaohongshu.com/*`, `xhslink.com/*`
- Bilibili: `bilibili.com/*`, `b23.tv/*`
- LinkedIn: `linkedin.com/*` (posts, profile URLs)

All tools accept these hosts and handle **video, image, carousel, slideshow
and text** posts.

## Troubleshooting

- **`Not authenticated with nooticr` / 401**: run `npx @nooticr/mcp login`, or
  set `NOOTICR_API_KEY` (see [Running on a server](#running-on-a-server-no-browser)).
- **`unknown, revoked or expired nooticr API key`**: the key is not one this
  account has live. `npx @nooticr/mcp api-key list` shows every key and its
  state, revoked ones included.
- **`API keys cannot manage API keys`**: `api-key create`/`list`/`revoke` need a
  signed-in session, which is why they ignore a key in `NOOTICR_API_KEY` *and*
  one stored by `login --api-key`. Run `npx @nooticr/mcp login` first.
- **402 paywall / `insufficient MCP credits`**: your nooticr account is out of
  credits. Prices are listed per tool in [Tools](#tools) — 1 credit for a post
  lookup or transcript, 2 for discovery and for a tool that makes one fetch, 3
  for the two that fetch frames *and* transcript. Every tool bills from the
  first call. Top up via
  `check_nooticr_credits`, or the nooticr dashboard at
  `https://nooticr.com/settings?tab=billing`.
- **Expired refresh token**: the stored refresh token was rejected by the
  nooticr server. Run `npx @nooticr/mcp login` again to re-authenticate.
- **`Could not reach the nooticr server`**: `NOOTICR_BASE_URL` is unreachable or
  wrong.
- **Client shows "Bad Request" or connection errors in HTTP mode**: make sure
  the port matches `NOOTICR_PUBLIC_URL` and that the client fetched a token
  first (the OAuth flow must complete once in your browser).

## Platform submissions

See [docs/SUBMISSION.md](docs/SUBMISSION.md) for ready-to-paste configs and the submission
package for **Claude Desktop**, **Cursor**, and **OpenAI Agents SDK**.

The official MCP registry entry `com.nooticr/mcp` must equal `server.json`'s version after each
release; see [docs/registry-publishing.md](docs/registry-publishing.md) for the drift check
(`npm run registry:check`) and the publish script (`npm run registry:publish`).

## Security notes

- The credentials file is written with mode `0600` and the directory with
  `0700`.
- OAuth redirect URIs are restricted to loopback (`http://localhost`,
  `http://127.0.0.1`, `http://[::1]`) or `https://` URLs; the `/authorize`
  endpoint requires PKCE (`S256`).
- Authorization codes and PKCE challenges are one-time use and short-lived
  (in-memory).
- Access tokens are opaque, random, and bound to the in-memory session map;
  they expire after 1 hour. Restarting the server invalidates all sessions.
- API keys are stored as a sha256 hash, never in the clear, so a database dump
  cannot be replayed against the API. They are scoped to one account and one
  workspace, carry their owner's role as it is *now* rather than as it was when
  the key was minted, and cannot be used to create or revoke keys.
- Never share your credentials file, `NOOTICR_ACCESS_TOKEN` or
  `NOOTICR_API_KEY`.

## Development

```bash
npm install
npm run build    # tsc
npm test         # vitest (66 unit tests, mocked fetch — no network)
```

Those are unit tests against a stub backend. For real MCP-protocol
end-to-end tests — no Rust, no Postgres, no API key —
`npm run test:e2e-smoke:fixture` spawns this repo's real built CLI and
drives it over stdio against a pure-Node stand-in for nooticr-server. See
`docs/testing/agentic-e2e-testing.md` for that plus the tier above it
(`npm run test:e2e-smoke` against a real nooticr-server). For a real model
driving the server through whole journeys — asserting on the chain of tools
it walks, not on one call — see `npm run test:quests` and
`docs/testing/tool-chaining-quests.md`. And for
`tests/e2e/agentic-visual*.e2e.ts` — every reachable widget view, driven by
a real tool-call result, rendered in a real browser with every host-facing
button actually clicked (`npx playwright test tests/e2e/agentic-visual.e2e.ts
tests/e2e/agentic-visual-full-app.e2e.ts`). That exercise found six real
product bugs, documented in the same doc.

## License

MIT

TDQS

A4/5.0

Scored across 27 tools

Disambiguation3/5

Most tools have explicit cross-references that separate discovery, fetching, analysis, and creation, which helps an agent choose correctly. However, analyze_post and understand_social_post overlap heavily in inputs, platforms, credit cost, and output focus, and the many analyze/discover/creator tools still require careful reading to avoid misselection.

Naming Consistency4/5

The overwhelming majority follow a clear snake_case verb_noun pattern like get_user_posts, analyze_comments, and write_hooks. Minor deviations such as analyze_post_fast, niche_report, and orchyn_login break the pattern slightly but remain readable and predictable.

Tool Count3/5

27 tools is on the heavy side, and some sub-areas like post analysis, creator research, and content generation could be consolidated. The broad scope across eight social platforms and the paid-credit workflow partly justifies the count, so it feels bloated but not chaotic.

Completeness4/5

The domain is well covered: discovery, media/transcript/comment retrieval, multi-level analysis, content creation, creator watchlists, and credit/auth management are all present. Minor gaps exist, such as no direct way to list watched creators or fetch a creator's raw profile stats without triggering analysis, but agents can work around them.

Maintenance

ActivityActive
ResponsivenessUnresponsive