viralhunt-mcp 0.3.0 → 1.1.11

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.
Files changed (4) hide show
  1. package/README.md +18 -1
  2. package/index.d.ts +63 -0
  3. package/index.js +98 -19
  4. package/package.json +15 -2
package/README.md CHANGED
@@ -5,7 +5,7 @@ all-in-one platform to **discover what's trending, curate it with your team, and
5
5
  A BuzzSumo alternative with a built-in cross-network scheduler and an API that AI agents can drive
6
6
  end to end.
7
7
 
8
- Get a free token: **https://viralhunt.io/claude** → create the free account (free forever, no card, 24 content queries a day), then Account → API Access. Full API
8
+ Get a free token: **https://viralhunt.io/claude** → create the free account (free forever, no card, 24 content queries a day), then Account → API Access → Create access. Every token has a **permission group** (reader, writer, reviewer, publisher, full): a tool outside the group answers `403 insufficient_scope` naming what it lacks, and `viralhunt_account` shows the token its own group. Give an agent that writes a writer token and the one that reviews a reviewer token. Full API
9
9
  docs: **https://viralhunt.io/api**.
10
10
 
11
11
  ## What an agent can do through this MCP server
@@ -27,6 +27,11 @@ much to trust it:
27
27
  | `viralhunt_schedule` | **Publish now, schedule, or leave a draft** (text + media, per-network copy, a first comment, or a `design` on a template the app renders when a person approves) |
28
28
  | `viralhunt_drafts` | **The drafts waiting** for a review and an approval, with their reviews |
29
29
  | `viralhunt_review_draft` | **Review a draft** — verdict, score, per-network warnings, a note (an agent as the team's checker) |
30
+ | `viralhunt_policy` | **The content rule** a reviewer applies before any OK: block and fix lists with codes, six scores and thresholds, per-network notes (read it first, every pass) |
31
+ | `viralhunt_review_queue` | **What needs a review**: posts in Review with no verdict on their current content |
32
+ | `viralhunt_calendar` | **The days worth a post**: international days and science/tech/space/health anniversaries with why, by window and category |
33
+ | `viralhunt_news_categories` | **The news categories** with counts; pass one as `category` to `viralhunt_trending` (rss) |
34
+ | `viralhunt_review_stats` | **Flags per collaborator** by week, fortnight or month: reviews, ok / fix / block, rule codes, average risk10 |
30
35
  | `viralhunt_approve_draft` | **Send a draft** (owner/admin token, with the user's yes), optionally with its translations |
31
36
  | `viralhunt_translate_post` | **The same post in another language** — the app adapts the copy and the template's texts for a linked project |
32
37
  | `viralhunt_edit_log` | **Learn from the edits** — what people changed in drafts after the agent left them |
@@ -53,6 +58,18 @@ network's media ceilings, and is told not to repost fake news / copyrighted medi
53
58
 
54
59
  ## Changelog
55
60
 
61
+ - **1.1.11 (2026-10-08)** — approve by a member token on its own graded post (when ticked on Team); review_stats carries avg_score, self_published, published_over_fix.
62
+ - **1.1.10 (2026-10-07)** — the reviewer's edit boundary (overrides and kind only, when trusted); text_limit per account.
63
+ - **1.1.9 (2026-10-07)** — `kind` on create and update (fact | meme | quote | opinion | promo): the reviewer applies only that kind's rules.
64
+ - **1.1.8 (2026-10-07)** — `overrides[account]` takes `first_comment` and `thread` per account; Bluesky/Mastodon threads as a reply chain.
65
+ - **1.1.7 (2026-10-07)** — `author_notes` on create and update: the source link for the reviewer, never published.
66
+ - **1.1.6 (2026-10-07)** — the safety catch on a trusted reviewer's OK (risk_threshold), project_lang on drafts, review_stats for trusted reviewers.
67
+ - **1.1.5 (2026-10-07)** — `viralhunt_calendar`: the calendar base (international days and anniversaries, with why), mark_used.
68
+ - **1.1.4 (2026-10-07)** — `viralhunt_news_categories` and `category` on `viralhunt_trending` (rss); unknown parameters are a 422 on the API.
69
+ - **1.1.3 (2026-10-07)** — the policy describes the kinds (fact, meme, quote, opinion, promo): a meme or a quote never gets a source or DOI demand.
70
+ - **1.1.2 (2026-10-06)** — `content_hash` on `viralhunt_review_draft` (409 content_changed when the post moved under the reviewer); self review is by user, not token.
71
+ - **1.1.1 (2026-10-06)** — the reviewer's four extra checks (picture against text, spelling, AI-written, facts with the DOI rule), `risk10` 1 to 10 on every review, `viralhunt_review_stats` (flags per collaborator).
72
+ - **1.1.0 (2026-10-05)** — the reviewer job: `viralhunt_policy`, `viralhunt_review_queue`, `needs_review` on drafts; the server recalculates the verdict and names in `sent.skipped` the rule that kept an OK from sending.
56
73
  - **0.3.0 (2026-09-28)** — drafts and review (`viralhunt_drafts`, `viralhunt_review_draft`,
57
74
  `viralhunt_approve_draft`, `viralhunt_edit_log`), translation into a linked project's language
58
75
  (`viralhunt_translate_post`), `design` on a post and `png` on update, `viralhunt_search`,
package/index.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * viralhunt-mcp — type declarations.
3
+ *
4
+ * The package is a command-line MCP server (`npx viralhunt-mcp`, speaking the Model Context Protocol over
5
+ * stdio); it exposes no programmatic API. These declarations exist so TypeScript consumers that import the
6
+ * module for its side effects, and the package registries that look for types, find them.
7
+ */
8
+ export {};
9
+
10
+ /** Environment the server reads at start-up. */
11
+ export interface ViralHuntMcpEnv {
12
+ /** A ViralHunt API token (vhk_…) from Account → API Access. Required. */
13
+ VIRALHUNT_API_KEY: string;
14
+ /** API base URL. Default: https://viralhunt.io/tool/api/v1 */
15
+ VIRALHUNT_API_BASE?: string;
16
+ }
17
+
18
+ /** The tools the server registers, by name. */
19
+ export type ViralHuntTool =
20
+ | 'viralhunt_account'
21
+ | 'viralhunt_approve_draft'
22
+ | 'viralhunt_assign_template'
23
+ | 'viralhunt_best_communities'
24
+ | 'viralhunt_best_time'
25
+ | 'viralhunt_board_context'
26
+ | 'viralhunt_cancel_post'
27
+ | 'viralhunt_card_comments'
28
+ | 'viralhunt_create_card'
29
+ | 'viralhunt_drafts'
30
+ | 'viralhunt_edit_log'
31
+ | 'viralhunt_get_post'
32
+ | 'viralhunt_get_template'
33
+ | 'viralhunt_list_templates'
34
+ | 'viralhunt_mark_quote_used'
35
+ | 'viralhunt_move_card'
36
+ | 'viralhunt_my_cards'
37
+ | 'viralhunt_policy'
38
+ | 'viralhunt_quotes'
39
+ | 'viralhunt_recipe_ran'
40
+ | 'viralhunt_recipes'
41
+ | 'viralhunt_review_draft'
42
+ | 'viralhunt_review_queue'
43
+ | 'viralhunt_calendar'
44
+ | 'viralhunt_news_categories'
45
+ | 'viralhunt_review_stats'
46
+ | 'viralhunt_schedule'
47
+ | 'viralhunt_search'
48
+ | 'viralhunt_stats'
49
+ | 'viralhunt_submit_draft'
50
+ | 'viralhunt_sync_post'
51
+ | 'viralhunt_targets'
52
+ | 'viralhunt_top_hashtags'
53
+ | 'viralhunt_translate_post'
54
+ | 'viralhunt_trending'
55
+ | 'viralhunt_trending_sounds'
56
+ | 'viralhunt_update_post'
57
+ | 'viralhunt_upsert_template';
58
+
59
+ /** Where a post sits before it is sent: being worked on, or complete and waiting for an owner or admin. */
60
+ export type PostStage = 'draft' | 'review';
61
+
62
+ /** The status of a scheduled post as the API reports it. */
63
+ export type PostStatus = PostStage | 'scheduled' | 'processing' | 'published' | 'partial' | 'failed' | 'canceled';
package/index.js CHANGED
@@ -79,13 +79,20 @@ tool(server, "viralhunt_trending",
79
79
  sort: z.string().optional().describe("viral (default), engagement, newest, oldest, plus per network: most_liked, most_viewed, most_commented, most_retweeted, most_reposted, most_saved, most_upvoted (reddit), most_boosted (mastodon), most_noted (tumblr), most_points (hackernews); rss: trending, engagement, growth, bluesky, mentions, coverage, hn, comments."),
80
80
  time_range: z.string().optional().describe("Time window on the post's own publish date: any number with a unit (24h, 7d, 30d, 2w, 3m, 1y), a bare number of days, or all (default 7d). An unknown value is a 422. Tumblr's corpus fills slowly: use 30d or all there."),
81
81
  keyword: z.string().optional().describe("Filter by keyword/niche (matched in title, text, hashtags or author)."),
82
+ category: z.string().optional().describe("rss only: ONE news category, a name or id from viralhunt_news_categories (exact name, case-insensitive; unknown = 422 with the valid names). Technology and Artificial Intelligence are two categories: for AI content call twice."),
82
83
  author: z.string().optional().describe("ONE page or account only (facebook page name, x handle or name, tiktok or instagram username; contains-match). 'The most viral posts of the page X in 2025' = source + author + time_range=1y."),
83
84
  subreddit: z.string().optional().describe("reddit only: restrict to one subreddit (with or without r/)."),
84
85
  min_engagement: z.number().int().min(0).optional().describe("Minimum engagement on the network's main metric."),
85
86
  per_page: z.number().int().min(1).max(100).optional().describe("How many results (max 100)."),
86
87
  page: z.number().int().min(1).optional().describe("Page number for more results."),
87
88
  },
88
- (a) => vh("/trending.php", { query: { source: a.source, sort: a.sort || "viral", time_range: a.time_range || "7d", keyword: a.keyword, author: a.author, subreddit: a.subreddit, min_engagement: a.min_engagement, per_page: a.per_page || 20, page: a.page } })
89
+ (a) => vh("/trending.php", { query: { source: a.source, sort: a.sort || "viral", time_range: a.time_range || "7d", keyword: a.keyword, category: a.category, author: a.author, subreddit: a.subreddit, min_engagement: a.min_engagement, per_page: a.per_page || 20, page: a.page } })
90
+ );
91
+
92
+ tool(server, "viralhunt_news_categories",
93
+ "The categories of the news corpus (source rss), with how many feeds carry each and how many articles arrived in the last 7 and 30 days. Read it once before asking for news on a subject, then pass a name as category on viralhunt_trending (source rss). Not the Editorial Board categories. Technology and Artificial Intelligence are separate categories.",
94
+ {},
95
+ () => vh("/news-categories.php")
89
96
  );
90
97
 
91
98
  // ── Who is this key: plan, quota, credits ──
@@ -121,7 +128,9 @@ tool(server, "viralhunt_targets",
121
128
  "List the user's projects/brands and the connected social accounts you can publish to in each. Also returns needs_reconnect",
122
129
  "(expired connections: not targets until a person reconnects them in the app; name the network before publishing), media_limits",
123
130
  "per network (image bytes: bigger ones are re-encoded to fit; video bytes and seconds: a bigger or longer video is refused for that",
124
- "network only, with the numbers; text length), and each project's lang and translates_to (linked projects in another language).",
131
+ "network only, with the numbers; text length), each project's lang and translates_to (linked projects in another language), and",
132
+ "must_draft: '' = this token may publish directly; 'role' = a member or agent token: every post waits in Drafts for an owner or admin",
133
+ "whatever draft says; 'review_mode' = the organization reviews every post. Read it before promising 'published'.",
125
134
  ].join(" "),
126
135
  { project: z.string().optional().describe("Project name to scope the accounts to (optional).") },
127
136
  (a) => vh("/schedule.php", { query: { action: "targets", project: a.project } })
@@ -135,7 +144,8 @@ tool(server, "viralhunt_schedule",
135
144
  "news, copyrighted media or spam. Never use another page's text as the copy or its branded picture as the post's picture: the",
136
145
  "picture comes from the organization's templates (viralhunt_get_template), either rendered by you and passed in media, or passed",
137
146
  "as `design` with draft=true so the app renders it when a person approves. Returns {id, status, results, warnings, review_url}:",
138
- "status processing|scheduled|draft|partial|failed; results keyed by account id carry each network's own reason when it refused.",
147
+ "status processing|scheduled|review|draft|partial|failed; results keyed by account id carry each network's own reason when it refused.",
148
+ "Two stages sit before sending: Drafts (being worked on) and Review (complete, dated, waiting for an owner or admin). Say which one the post is in.",
139
149
  ].join(" "),
140
150
  {
141
151
  project: z.string().optional().describe("Project name — REQUIRED if the org has more than one project."),
@@ -146,46 +156,85 @@ tool(server, "viralhunt_schedule",
146
156
  target_account_ids: z.array(z.number().int()).optional().describe("Exact account ids (from viralhunt_targets). Omit = all accounts in the project."),
147
157
  scheduled_at: z.string().optional().describe("ISO-8601 UTC, e.g. 2026-08-01T15:30:00Z. Omit = publish now (or, with draft, the tentative time is 'when approved')."),
148
158
  first_comment: z.string().optional().describe("Optional first comment (link-in-comments)."),
149
- draft: z.boolean().optional().describe("true = save it in Drafts for a person to review and approve; nothing is sent. Use it unless the user explicitly asked to publish now."),
150
- overrides: z.object({}).passthrough().optional().describe('Per-network (or per account id) copy or media: {"bluesky": {"body": "short version"}, "12": {"media": ["…"]}}. Bluesky takes 300 characters, X 280.'),
159
+ author_notes: z.string().optional().describe("For the reviewer, never published: the source link, the picture's origin, what you verified. Put the source link HERE, never in the body (the networks cut the reach)."),
160
+ kind: z.enum(["fact", "meme", "quote", "opinion", "promo"]).optional().describe("What the post IS. The reviewer applies only that kind's rules (a meme is never asked for a source). A post from a card takes the card's category when omitted."),
161
+ draft: z.boolean().optional().describe("true = a working copy in Drafts you still mean to edit (viralhunt_update_post, then viralhunt_submit_draft). Without it, a member or agent token lands the complete post in Review, where an owner or admin authorizes it; nothing is sent either way. An owner token publishes directly."),
162
+ overrides: z.object({}).passthrough().optional().describe('Per network (or per account id), only what that account needs to differ: {"bluesky": {"body": "the 300-character version", "thread": [{"body": "part 2"}]}, "12": {"media": ["…"], "first_comment": "its own first comment"}}. body, media, first_comment, thread. X 280, Bluesky 300, Threads 500, Mastodon 500. A thread on Bluesky/Mastodon is a reply chain posted after the post is live.'),
151
163
  card_id: z.number().int().optional().describe("The Editorial Board card this post comes from (optional)."),
152
164
  design: z.object({}).passthrough().optional().describe('The picture as a filled template instead of a rendered file: {"template": "<slug or id>", "format": "feed", "variables": {"text": "…", "highlight": "…", "image": "https://…", "caption": "…"}}. Needs draft=true when media is empty; the app renders it when a person approves.'),
153
165
  },
154
166
  (a) => vh("/schedule.php", { method: "POST", query: { action: "create" }, body: {
155
167
  project: a.project, project_id: a.project_id, body: a.body, media: a.media,
156
- networks: a.networks, target_account_ids: a.target_account_ids, scheduled_at: a.scheduled_at, first_comment: a.first_comment,
168
+ networks: a.networks, target_account_ids: a.target_account_ids, scheduled_at: a.scheduled_at, first_comment: a.first_comment, author_notes: a.author_notes, kind: a.kind,
157
169
  draft: a.draft ? true : undefined, overrides: a.overrides, card_id: a.card_id, design: a.design,
158
170
  } })
159
171
  );
160
172
 
161
173
  // ── Drafts: the limbo before sending ──
162
174
  tool(server, "viralhunt_drafts",
163
- "List the drafts waiting for a review and an approval (this project, or every project with all=true). Each carries body, media, targets, scheduled_at, overrides, card_id, submitted_via (api = from an agent), design, lang, translated_from_id and review {score, verdict, reviewed_at, entries[]}.",
175
+ "List the posts not yet sent (this project, or every project with all=true): stage 'draft' = being worked on, 'review' = complete and dated, waiting for an owner or admin. Each carries stage, body, media, targets, scheduled_at, overrides, card_id, submitted_via (api = from an agent), design, lang, translated_from_id, review {score, verdict, reviewed_at, entries[]}, needs_review (no verdict on its current content), valid_verdict, content_hash and updated_at.",
164
176
  {
165
177
  project: z.string().optional().describe("Project name (default: the org's first project)."),
166
178
  project_id: z.number().int().optional(),
167
179
  all: z.boolean().optional().describe("true = every project of the organization."),
180
+ stage: z.enum(["draft", "review"]).optional().describe("One stage only (default: both)."),
181
+ needs_review: z.boolean().optional().describe("true = only posts with no verdict on their current content (the reviewer's queue)."),
168
182
  },
169
- (a) => vh("/schedule.php", { query: { action: "drafts", project: a.project, project_id: a.project_id, all: a.all ? 1 : undefined } })
183
+ (a) => vh("/schedule.php", { query: { action: "drafts", project: a.project, project_id: a.project_id, all: a.all ? 1 : undefined, stage: a.stage, needs_review: a.needs_review ? 1 : undefined } })
184
+ );
185
+
186
+ tool(server, "viralhunt_policy",
187
+ "The content rule a reviewer applies before any OK, kept on the platform so it changes without a release: what blocks a post (policy_* codes), what sends it back to Drafts (fix_* codes), the six scores (tos_risk, rights, fake_news, sensationalism, brand, grammar; 100 = clean) with the thresholds the server recalculates the verdict with, the extra readings every review carries (risk10 1 safe to 10 breaks a network's terms, image_match, ai_written), the kinds (fact, meme, quote, opinion, promo: a meme or a quote never gets a source or DOI demand), what changes per network, and the server rules an OK cannot bypass. Read it at the start of every review pass; never judge from memory.",
188
+ {},
189
+ () => vh("/policy.php")
190
+ );
191
+
192
+ tool(server, "viralhunt_review_queue",
193
+ "The reviewer's queue: posts in Review, every project, with no verdict on their current content (never reviewed, or edited after the last verdict). Same rows as viralhunt_drafts. Read viralhunt_policy first, then one viralhunt_review_draft per post.",
194
+ {
195
+ project_id: z.number().int().optional().describe("One project only (default: every project)."),
196
+ },
197
+ (a) => vh("/schedule.php", { query: { action: "drafts", stage: "review", all: a.project_id ? undefined : 1, project_id: a.project_id, needs_review: 1 } })
198
+ );
199
+
200
+ tool(server, "viralhunt_review_stats",
201
+ "Flags per collaborator (owner, admin, or a reviewer marked 'Their OK publishes'): for the last days (7 = week, 14 = fortnight, 30 = month), per person or agent who authored posts: reviews, ok / fix / block, flags (a fix or block, or any policy_* / fix_* warning), codes (how many of each rule, e.g. fix_doi), avg_risk10 and last_flag_at. The Team page shows the same table.",
202
+ {
203
+ days: z.number().int().optional().describe("7, 14 or 30 (default 30)."),
204
+ },
205
+ (a) => vh("/schedule.php", { query: { action: "review_stats", days: a.days } })
206
+ );
207
+
208
+ tool(server, "viralhunt_submit_draft",
209
+ "Move a draft to Review once it is complete (a body or media, and at least one account): it waits there for an owner or admin to authorize it. now=true clears the time (it goes out when authorized); scheduled_at sets one; neither keeps the draft's own. Nothing is sent.",
210
+ {
211
+ id: z.number().int().describe("The draft id."),
212
+ now: z.boolean().optional(),
213
+ scheduled_at: z.string().optional().describe("ISO-8601 UTC."),
214
+ },
215
+ (a) => vh("/schedule.php", { method: "POST", query: { action: "submit" }, body: { id: a.id, now: a.now ? 1 : undefined, scheduled_at: a.scheduled_at } })
170
216
  );
171
217
 
172
218
  tool(server, "viralhunt_review_draft",
173
219
  [
174
- "Append your review to a draft: verdict ok|fix|block, a score 0-100, per-topic scores (tos_risk, fake_news, sensationalism, grammar),",
175
- "one warning per issue with the network it concerns, and a short note on how to fix it. Judge each target network's terms (violence,",
176
- "health claims, politics, minors, copyright, spam), unverified claims (cross-check with viralhunt_search / trending rss), sensationalism,",
177
- "grammar. `block` only for what must not go out as it is. If the organization sends drafts by themselves on an OK review, an `ok` verdict",
178
- "SENDS the draft and the answer carries `sent`: give ok only when you would approve it yourself, and tell the user.",
220
+ "Append your review to a post in Review: verdict ok|fix|block, a score 0-100, per-topic scores (tos_risk, rights, fake_news, sensationalism, brand, grammar; 100 = clean) plus risk10 (1 safe to 10 breaks a network's terms), image_match and ai_written (0-100),",
221
+ "one warning per issue with the policy's code (policy_* / fix_*), the network it concerns and the severity, and a short note on how to fix it, in the post's language.",
222
+ "Judge against viralhunt_policy (read it first), network by network; verify claims with viralhunt_search / trending rss. `fix` sends the post back to Drafts with your note",
223
+ "(returned: true). The server recalculates the verdict from your scores and warnings with the policy thresholds and keeps the more severe one (verdict, verdict_requested,",
224
+ "verdict_reason in the answer). An `ok` sends the post at its time only when the organization sends on OK AND this token may release (owner, admin, or 'their OK publishes' in Team);",
225
+ "The note carries the fix (the corrected sentence, the exact link, the short version); say which problem decided the verdict. The server refuses to send on a self review (same USER, not token: the reviewer is its own agent member), a post with no time or under 15 minutes, one edited after the review, or one whose scores sit outside the policy's auto_send thresholds (risk10 > 5, fake_news < 70, image_match < 60: risk_threshold, a person decides), and says why in sent.skipped. Report `sent` as it is. Drafts rows carry project_lang: a post in another language is fix_language.",
226
+ "Always pass content_hash from the row you read: if the post changed meanwhile the server answers 409 content_changed with the current hash and writes nothing; read it again.",
179
227
  ].join(" "),
180
228
  {
181
229
  id: z.number().int().describe("The draft id."),
182
230
  verdict: z.enum(["ok", "fix", "block"]),
231
+ content_hash: z.string().optional().describe("The content_hash of the drafts row when you read the post. Always send it."),
183
232
  score: z.number().int().min(0).max(100).optional(),
184
- scores: z.object({}).passthrough().optional().describe('{"tos_risk": 90, "fake_news": 70, "sensationalism": 80, "grammar": 95}'),
233
+ scores: z.object({}).passthrough().optional().describe('{"tos_risk": 90, "fake_news": 70, "sensationalism": 80, "grammar": 95, "risk10": 3, "image_match": 85, "ai_written": 40}'),
185
234
  warnings: z.array(z.object({ code: z.string().optional(), network: z.string().optional(), text: z.string(), severity: z.enum(["info", "warn", "block"]).optional() })).optional(),
186
235
  note: z.string().optional().describe("How to fix it, and the source you verified against."),
187
236
  },
188
- (a) => vh("/schedule.php", { method: "POST", query: { action: "review" }, body: { id: a.id, verdict: a.verdict, score: a.score, scores: a.scores, warnings: a.warnings, note: a.note } })
237
+ (a) => vh("/schedule.php", { method: "POST", query: { action: "review" }, body: { id: a.id, verdict: a.verdict, score: a.score, scores: a.scores, warnings: a.warnings, note: a.note, content_hash: a.content_hash } })
189
238
  );
190
239
 
191
240
  tool(server, "viralhunt_approve_draft",
@@ -239,7 +288,7 @@ tool(server, "viralhunt_sync_post",
239
288
  // ── Check status ──
240
289
  tool(server, "viralhunt_get_post",
241
290
  [
242
- "Get the current status and per-network results (incl. permalinks) of a post you created. Statuses: scheduled (queued), processing",
291
+ "Get the current status and per-network results (incl. permalinks) of a post you created. Also events[] (the delivery log: created, accepted by PostProxy, published with its link, refused with the reason, retried, canceled, reviewed, approved) and support_text, the text to hand the user when they ask what happened to a post. Statuses: scheduled (queued), processing",
243
292
  "(handed to the networks, or a target being retried), published (every target live), partial (some refused: each failed",
244
293
  "results[account].error carries the network's own words and error_message sums it up), failed, draft, canceled. The app retries a",
245
294
  "transient failure up to three times (results show retries, will_retry, retry_after); a final reason (dead connection, refused text,",
@@ -252,7 +301,8 @@ tool(server, "viralhunt_get_post",
252
301
  // ── Edit a scheduled post or a draft ──
253
302
  tool(server, "viralhunt_update_post",
254
303
  [
255
- "Edit a post. A DRAFT can be changed freely (body, media, overrides, first_comment, targets, scheduled_at, design, and png: the",
304
+ "Edit a post. A DRAFT can be changed freely by its author, an owner or an admin (body, media, overrides, first_comment, targets, scheduled_at, design, and png: the",
305
+ "a reviewer marked 'Their OK publishes' may change only overrides (a per-account short copy) and kind on someone else's post);",
256
306
  "rendered picture of its design as a data:image/png;base64 URL, which becomes the post's first picture). A SCHEDULED post can be",
257
307
  "edited only while status is 'scheduled' and more than 5 min before publish. Only the fields you send change; the project cannot",
258
308
  "be changed. Check status with viralhunt_get_post first; on a 409 error re-fetch instead of retrying.",
@@ -265,13 +315,15 @@ tool(server, "viralhunt_update_post",
265
315
  target_account_ids: z.array(z.number().int()).optional().describe("New exact account-id set."),
266
316
  scheduled_at: z.string().optional().describe("New ISO-8601 UTC time (must be in the future)."),
267
317
  first_comment: z.string().optional().describe("Drafts: the first comment."),
268
- overrides: z.object({}).passthrough().optional().describe("Drafts: per-network copy or media."),
318
+ author_notes: z.string().optional().describe("Drafts: notes for the reviewer, never published (the source link goes here)."),
319
+ kind: z.enum(["fact", "meme", "quote", "opinion", "promo"]).optional().describe("Drafts: what the post is."),
320
+ overrides: z.object({}).passthrough().optional().describe("Drafts: per network or per account id: body, media, first_comment, thread."),
269
321
  design: z.object({}).passthrough().optional().describe("Drafts: the filled template (see viralhunt_schedule)."),
270
322
  png: z.string().optional().describe("Drafts: data:image/png;base64,… of the rendered design; stored and made the post's first picture."),
271
323
  },
272
324
  (a) => vh("/schedule.php", { method: "POST", query: { action: "update" }, body: {
273
325
  id: a.id, body: a.body, media: a.media, networks: a.networks, target_account_ids: a.target_account_ids, scheduled_at: a.scheduled_at,
274
- first_comment: a.first_comment, overrides: a.overrides, design: a.design, png: a.png,
326
+ first_comment: a.first_comment, author_notes: a.author_notes, kind: a.kind, overrides: a.overrides, design: a.design, png: a.png,
275
327
  } })
276
328
  );
277
329
 
@@ -289,6 +341,33 @@ tool(server, "viralhunt_stats",
289
341
  );
290
342
 
291
343
  // ── Quotes base ──
344
+ tool(server, "viralhunt_calendar",
345
+ [
346
+ "The calendar base: the international days (UN, UNESCO, WHO) and the anniversaries of science, technology, space, health, the environment and civilization",
347
+ "in a window of days, each with `why` (what is celebrated or what happened, and why it matters: write the caption FROM it, never from memory),",
348
+ "kind (observance repeats every year; event, birth, death carry year and years_ago), category, origin, importance (3 = most brands can post about it,",
349
+ "2 = worth it inside its category, 1 = niche), image_url (a free Wikipedia thumbnail when there is one), source_url. Use days=7 or 31 to plan ahead;",
350
+ "post on the day. The vh-ephemeris template fits an event or a birth; the news frame fits an observance. mark_used=true with calendar_id records what you posted.",
351
+ ].join(" "),
352
+ {
353
+ date: z.string().optional().describe("YYYY-MM-DD or today (default today, UTC)."),
354
+ days: z.number().int().min(1).max(92).optional().describe("How many days from date (default 1)."),
355
+ category: z.string().optional().describe("One or several: science,technology,space,health,environment,humanity,civilization,education,culture,economy."),
356
+ kind: z.enum(["observance", "event", "birth", "death"]).optional(),
357
+ min_importance: z.number().int().min(1).max(3).optional().describe("3 = the big days only, 2 = worth a post (recommended), 1 = everything."),
358
+ lang: z.enum(["en", "es"]).optional(),
359
+ q: z.string().optional().describe("Keyword in the title, the text or the tags."),
360
+ unused: z.boolean().optional().describe("true = leave out what this organization already posted about this year."),
361
+ stats: z.boolean().optional().describe("true = how many rows per category and kind instead of the rows."),
362
+ mark_used: z.boolean().optional().describe("true with calendar_id = record that you posted about it this year."),
363
+ calendar_id: z.number().int().optional(),
364
+ note: z.string().optional(),
365
+ },
366
+ (a) => a.mark_used
367
+ ? vh("/calendar.php", { method: "POST", body: { action: "mark_used", calendar_id: a.calendar_id, note: a.note } })
368
+ : vh("/calendar.php", { query: { date: a.date, days: a.days, category: a.category, kind: a.kind, min_importance: a.min_importance, lang: a.lang, q: a.q, unused: a.unused ? 1 : undefined, stats: a.stats ? 1 : undefined } })
369
+ );
370
+
292
371
  tool(server, "viralhunt_quotes",
293
372
  [
294
373
  "Quotes ranked by our popularity score, for daily-quote series: text, author, author_context (description, born, died), work,",
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "viralhunt-mcp",
3
3
  "mcpName": "io.github.rodvan/viralhunt-mcp",
4
- "version": "0.3.0",
5
- "description": "MCP server for ViralHunt.io: find trending/viral content across TikTok, Instagram, X, Facebook, Pinterest, Bluesky, Douyin, Reddit, Mastodon, Tumblr, Hacker News and news RSS; search one keyword on every network; learn the best time to post, top hashtags, trending sounds and the best communities; make on-brand images from the organization's templates; leave complete posts as drafts, review them, translate them into a linked project's language; and publish, schedule or edit posts on your connected social accounts.",
4
+ "version": "1.1.11",
5
+ "description": "MCP server for ViralHunt.io: find trending/viral content across TikTok, Instagram, X, Facebook, Pinterest, Bluesky, Douyin, Reddit, Mastodon, Tumblr, Hacker News and news RSS; search one keyword on every network; learn the best time to post, top hashtags, trending sounds and the best communities; make on-brand images from the organization's templates; leave complete posts as drafts, review them, translate them into a linked project's language; and publish, schedule or edit posts on your connected social accounts.",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "viralhunt-mcp": "index.js"
9
9
  },
10
+ "types": "index.d.ts",
10
11
  "files": [
11
12
  "index.js",
13
+ "index.d.ts",
12
14
  "README.md"
13
15
  ],
14
16
  "engines": {
@@ -24,7 +26,18 @@
24
26
  "buzzsumo-alternative"
25
27
  ],
26
28
  "homepage": "https://viralhunt.io",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/viralhunt-io/viralhunt-mcp.git"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/viralhunt-io/viralhunt-mcp/issues"
35
+ },
36
+ "author": "ViralHunt (https://viralhunt.io)",
27
37
  "license": "MIT",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
28
41
  "dependencies": {
29
42
  "@modelcontextprotocol/sdk": "^1.0.4",
30
43
  "zod": "^3.23.8"