viralhunt-mcp 0.2.1 → 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 +53 -11
  2. package/index.d.ts +63 -0
  3. package/index.js +310 -17
  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** → Account → API Access (7-day trial, no card). 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
@@ -16,18 +16,34 @@ much to trust it:
16
16
 
17
17
  | Tool | What it does |
18
18
  |------|--------------|
19
- | `viralhunt_trending` | **Find topics** — what's going viral on TikTok, Instagram, X, Facebook, Pinterest, Bluesky, Douyin, Reddit, Mastodon, Tumblr, Hacker News and news RSS, ranked by viral score, each post with `growth_24h` (how much it moved between our two most distant readings) |
19
+ | `viralhunt_account` | **Who is this key** — plan, daily quota left, credits, per-endpoint rules (call first) |
20
+ | `viralhunt_trending` | **Find topics** — what's going viral on TikTok, Instagram, X, Facebook, Pinterest, Bluesky, Douyin, Reddit, Mastodon, Tumblr, Hacker News and news RSS, ranked by viral score, each post with `growth_24h` (how much it moved between our two most distant readings); `author` narrows to one page or account |
21
+ | `viralhunt_search` | **One keyword, every network** — merged and ranked, with per-network totals |
20
22
  | `viralhunt_best_time` | **When to post** — best weekday + hour per network from the posts that went viral there (365-day sample, hit rate, sample size, your time zone, optional niche keyword) |
21
23
  | `viralhunt_top_hashtags` | **Which hashtags** — top tags per network or across all, by engagement, posts or per-post; one tag's breakdown by network |
22
24
  | `viralhunt_trending_sounds` | **Which sound** — trending audio on TikTok, Instagram Reels and Douyin, cross-network sounds first, with the posts that used it |
23
25
  | `viralhunt_best_communities` | **Where to post** — best subreddits (peak per 1,000 members, timing, top posts, similar) and Bluesky custom feeds for a topic |
24
- | `viralhunt_targets` | List your **brands/projects** and the connected accounts you can post to |
25
- | `viralhunt_schedule` | **Publish now or schedule** a post (text + media) across your connected accounts |
26
- | `viralhunt_get_post` | Check a post's status + per-network permalinks |
27
- | `viralhunt_update_post` | Edit a still-scheduled post (body / media / networks / time) |
28
- | `viralhunt_cancel_post` | Cancel the not-yet-published targets of a scheduled post |
29
- | `viralhunt_list_templates` | Browse the **content template** library (on-brand layouts you fill) |
30
- | `viralhunt_get_template` | Get one template's full spec (html + css + variable manifest) to render |
26
+ | `viralhunt_targets` | List your **brands/projects** and the connected accounts you can post to, with `needs_reconnect`, per-network `media_limits` and the projects linked in another language |
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
+ | `viralhunt_drafts` | **The drafts waiting** for a review and an approval, with their reviews |
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 |
35
+ | `viralhunt_approve_draft` | **Send a draft** (owner/admin token, with the user's yes), optionally with its translations |
36
+ | `viralhunt_translate_post` | **The same post in another language** — the app adapts the copy and the template's texts for a linked project |
37
+ | `viralhunt_edit_log` | **Learn from the edits** — what people changed in drafts after the agent left them |
38
+ | `viralhunt_get_post` | Check a post's status + per-network results, permalinks and each network's own reason when it refused |
39
+ | `viralhunt_sync_post` | Refresh statuses from the networks |
40
+ | `viralhunt_update_post` | Edit a draft (anything, incl. the rendered `png`) or a still-scheduled post (body / media / networks / time) |
41
+ | `viralhunt_cancel_post` | Cancel the not-yet-published targets of a scheduled post, or drop a draft |
42
+ | `viralhunt_stats` | **What performed** — engagement of the published posts by network, by brand, top posts |
43
+ | `viralhunt_quotes` / `viralhunt_mark_quote_used` | The **quotes base** for daily-quote series (public domain by default, author context, portraits) |
44
+ | `viralhunt_recipes` / `viralhunt_recipe_ran` | **Standing orders** a person saved in the app: source, template, cadence; stamp a run |
45
+ | `viralhunt_list_templates` | Browse the **content template** library (on-brand layouts you fill; favourites first) |
46
+ | `viralhunt_get_template` | Get one template's full spec (html + css + variable manifest, what is `static` and what is `dynamic`, what it `suits`, the owner's `instructions`) to render |
31
47
  | `viralhunt_upsert_template` | **Author or edit a template** — editing a curated one clones it into your copy (owner/admin) |
32
48
  | `viralhunt_assign_template` | Assign a template to a project so its agents can use it (owner/admin) |
33
49
  | `viralhunt_board_context` | The team's **Editorial Board** in one call: members (with ids), columns, categories |
@@ -36,8 +52,34 @@ much to trust it:
36
52
  | `viralhunt_my_cards` | The cards assigned to you (when the token belongs to an agent member of the team) |
37
53
  | `viralhunt_card_comments` | Read or add comments on a card (notifies the assignee, mirrors to team chat) |
38
54
 
39
- Guardrails baked in: it won't post to the wrong brand, only schedules in the future, and is told not to
40
- repost fake news / copyrighted media / spam.
55
+ Guardrails baked in: it won't post to the wrong brand, only schedules in the future, drafts by default
56
+ unless the user asked to publish now, never uses another page's text or branded picture, checks each
57
+ network's media ceilings, and is told not to repost fake news / copyrighted media / spam.
58
+
59
+ ## Changelog
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.
73
+ - **0.3.0 (2026-09-28)** — drafts and review (`viralhunt_drafts`, `viralhunt_review_draft`,
74
+ `viralhunt_approve_draft`, `viralhunt_edit_log`), translation into a linked project's language
75
+ (`viralhunt_translate_post`), `design` on a post and `png` on update, `viralhunt_search`,
76
+ `viralhunt_account`, `viralhunt_stats`, `viralhunt_quotes`, `viralhunt_recipes`, `viralhunt_sync_post`;
77
+ `author` and free-form `time_range` on trending; `media_limits`, `needs_reconnect` and language links on
78
+ targets; templates say what is static, dynamic and what they suit; honest post statuses with each
79
+ network's reason.
80
+ - **0.2.x (2026-09-08)** — best time to post, top hashtags, trending sounds, best communities, the editorial
81
+ board; Reddit, Mastodon, Tumblr and Hacker News on trending; `growth_24h` documented.
82
+ - **0.1.x (2026-08)** — trending, targets, publish/schedule, get, update, cancel, content templates.
41
83
 
42
84
  ## What ViralHunt the platform does (beyond this MCP)
43
85
 
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
@@ -68,59 +68,245 @@ tool(server, "viralhunt_trending",
68
68
  "Sources: tiktok, instagram, x, facebook, pinterest, bluesky, douyin, reddit, mastodon, tumblr, hackernews, and rss (news articles",
69
69
  "from 230 feeds plus articles discovered through social links, each with Facebook/Reddit/Bluesky/Hacker News/comment signals).",
70
70
  "Each post carries growth_24h when we hold at least two readings of it: {from, to, delta, percent, hours, measured_at, samples}.",
71
- "Read it carefully: growth_24h null means 'we cannot say', NOT zero (Facebook and Pinterest never carry it); `hours` is the REAL",
72
- "window between the two readings and is not always 24, so quote it; `percent` is null when the post started from zero while",
73
- "`delta` still holds the absolute change. A high delta over few hours is what 'going viral right now' looks like.",
71
+ "Read it carefully: growth_24h null means 'we cannot say', NOT zero (Facebook and Pinterest never carry it); delta 0 with",
72
+ "samples >= 2 means measured and unchanged (flat), not missing; `hours` is the REAL window between the two readings (the newest",
73
+ "reading at least 20h older than the latest, or the oldest one when the post is younger than that), so it is often under 24:",
74
+ "quote it, never assume 24, and say 'in 24h' only when `full_window` is true; `percent` is null when `from` is under 100 (from zero or from a handful of interactions a percentage",
75
+ "is noise) while `delta` always holds the absolute change. A high delta over few hours is what 'going viral right now' looks like.",
74
76
  ].join(" "),
75
77
  {
76
78
  source: z.enum(["tiktok", "instagram", "x", "facebook", "pinterest", "bluesky", "douyin", "reddit", "mastodon", "tumblr", "hackernews", "rss"]).describe("Which network to pull trending from."),
77
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."),
78
- time_range: z.enum(["6h", "12h", "24h", "7d", "30d", "3m", "all"]).optional().describe("Time window on the post's own publish date (default 7d). Tumblr's corpus fills slowly: use 30d or all for tumblr, 7d is often empty there."),
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."),
79
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."),
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."),
80
84
  subreddit: z.string().optional().describe("reddit only: restrict to one subreddit (with or without r/)."),
81
85
  min_engagement: z.number().int().min(0).optional().describe("Minimum engagement on the network's main metric."),
82
86
  per_page: z.number().int().min(1).max(100).optional().describe("How many results (max 100)."),
83
87
  page: z.number().int().min(1).optional().describe("Page number for more results."),
84
88
  },
85
- (a) => vh("/trending.php", { query: { source: a.source, sort: a.sort || "viral", time_range: a.time_range || "7d", keyword: a.keyword, 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")
96
+ );
97
+
98
+ // ── Who is this key: plan, quota, credits ──
99
+ tool(server, "viralhunt_account",
100
+ "Who this key is: plan (free/creator/studio/agency), requests per hour, whether it can publish, today's daily content-query quota (free plan: 24 shared across trending, search, best-time, hashtags, sounds, communities), credits balance, and per-endpoint rules. Call it once at the start and again on a 429 or 402, and set expectations from it (free plan: trending is 72h delayed and numbers are withheld, never print 0).",
101
+ {},
102
+ () => vh("/account.php")
103
+ );
104
+
105
+ // ── One keyword, every network ──
106
+ tool(server, "viralhunt_search",
107
+ [
108
+ "Search one keyword across every network at once (or a chosen subset) and get one merged, ranked list: each row normalized to",
109
+ "platform, id, title, description, post_url, thumbnail_url, author_name, author_handle, engagement_score, stats, growth_24h,",
110
+ "created_at, plus the network's full row under raw. sources[] says per network how many posts matched (total), so you can tell the",
111
+ "user where the topic lives and where the corpus holds nothing (total 0 = say so, never invent posts). All words must appear, in any order.",
112
+ "Use viralhunt_trending when the user wants ONE network with its own sorts and paging.",
113
+ ].join(" "),
114
+ {
115
+ keyword: z.string().describe("The words to find (all must appear)."),
116
+ sources: z.string().optional().describe("all (default) or a comma list: pinterest,x,reddit,tiktok,instagram,facebook,bluesky,douyin,mastodon,tumblr,hackernews,rss."),
117
+ time_range: z.string().optional().describe("Window on the post's own date (default 30d): a number with a unit, a bare number of days, or all."),
118
+ per_source: z.number().int().min(1).max(50).optional().describe("Rows per network (default 10)."),
119
+ sort: z.enum(["engagement", "recent"]).optional(),
120
+ min_engagement: z.number().int().min(0).optional(),
121
+ },
122
+ (a) => vh("/search.php", { query: { keyword: a.keyword, sources: a.sources, time_range: a.time_range, per_source: a.per_source, sort: a.sort, min_engagement: a.min_engagement } })
86
123
  );
87
124
 
88
125
  // ── Where can I publish (projects + connected accounts) ──
89
126
  tool(server, "viralhunt_targets",
90
- "List the user's projects/brands and the connected social accounts you can publish to in each.",
127
+ [
128
+ "List the user's projects/brands and the connected social accounts you can publish to in each. Also returns needs_reconnect",
129
+ "(expired connections: not targets until a person reconnects them in the app; name the network before publishing), media_limits",
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",
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'.",
134
+ ].join(" "),
91
135
  { project: z.string().optional().describe("Project name to scope the accounts to (optional).") },
92
136
  (a) => vh("/schedule.php", { query: { action: "targets", project: a.project } })
93
137
  );
94
138
 
95
- // ── Publish now or schedule ──
139
+ // ── Publish now, schedule, or leave a draft ──
96
140
  tool(server, "viralhunt_schedule",
97
- "Publish now or schedule a post (text + media URLs) to a project's connected accounts. Verify content before posting — never publish fake news, copyrighted media, or spam.",
141
+ [
142
+ "Publish now, schedule, or (default when the user did not ask to publish right now) leave a DRAFT of a post on a project's",
143
+ "connected accounts. Ask the user before publishing or scheduling: it posts to real accounts. Verify content first: never fake",
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",
145
+ "picture comes from the organization's templates (viralhunt_get_template), either rendered by you and passed in media, or passed",
146
+ "as `design` with draft=true so the app renders it when a person approves. Returns {id, status, results, warnings, review_url}:",
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.",
149
+ ].join(" "),
98
150
  {
99
151
  project: z.string().optional().describe("Project name — REQUIRED if the org has more than one project."),
100
152
  project_id: z.number().int().optional().describe("Project id — alternative to project name."),
101
- body: z.string().optional().describe("Caption/text. Optional if media is provided."),
102
- media: z.array(z.string()).optional().describe("Public image/video URLs."),
153
+ body: z.string().optional().describe("Caption/text. Optional if media or design is provided."),
154
+ media: z.array(z.string()).optional().describe("Public image/video URLs. Check media_limits (viralhunt_targets) before a big video."),
103
155
  networks: z.array(z.string()).optional().describe('Post to every account on these networks, e.g. ["instagram","facebook"].'),
104
156
  target_account_ids: z.array(z.number().int()).optional().describe("Exact account ids (from viralhunt_targets). Omit = all accounts in the project."),
105
- scheduled_at: z.string().optional().describe("ISO-8601 UTC, e.g. 2026-08-01T15:30:00Z. Omit = publish now."),
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')."),
106
158
  first_comment: z.string().optional().describe("Optional first comment (link-in-comments)."),
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.'),
163
+ card_id: z.number().int().optional().describe("The Editorial Board card this post comes from (optional)."),
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.'),
107
165
  },
108
166
  (a) => vh("/schedule.php", { method: "POST", query: { action: "create" }, body: {
109
167
  project: a.project, project_id: a.project_id, body: a.body, media: a.media,
110
- 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,
169
+ draft: a.draft ? true : undefined, overrides: a.overrides, card_id: a.card_id, design: a.design,
111
170
  } })
112
171
  );
113
172
 
173
+ // ── Drafts: the limbo before sending ──
174
+ tool(server, "viralhunt_drafts",
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.",
176
+ {
177
+ project: z.string().optional().describe("Project name (default: the org's first project)."),
178
+ project_id: z.number().int().optional(),
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)."),
182
+ },
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 } })
216
+ );
217
+
218
+ tool(server, "viralhunt_review_draft",
219
+ [
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.",
227
+ ].join(" "),
228
+ {
229
+ id: z.number().int().describe("The draft id."),
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."),
232
+ score: z.number().int().min(0).max(100).optional(),
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}'),
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(),
235
+ note: z.string().optional().describe("How to fix it, and the source you verified against."),
236
+ },
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 } })
238
+ );
239
+
240
+ tool(server, "viralhunt_approve_draft",
241
+ [
242
+ "Send a draft (owner or admin token only, and never without the user's explicit yes). A draft blocked by its last review cannot be",
243
+ "approved until it is fixed and reviewed again. `translations` makes the translated draft in each linked project in the same call",
244
+ "(see viralhunt_translate_post). Returns {status, results, warnings, translations[]}.",
245
+ ].join(" "),
246
+ {
247
+ id: z.number().int().describe("The draft id."),
248
+ translations: z.array(z.object({ project_id: z.number().int(), lang: z.string().optional(), account_ids: z.array(z.number().int()).optional(), image: z.string().optional(), send_mode: z.enum(["draft", "send"]).optional() })).optional(),
249
+ },
250
+ (a) => vh("/schedule.php", { method: "POST", query: { action: "approve" }, body: { id: a.id, translations: a.translations } })
251
+ );
252
+
253
+ tool(server, "viralhunt_translate_post",
254
+ [
255
+ "Make the translated draft of a post (a draft or one already sent) in a linked project that publishes in another language. The app",
256
+ "adapts the copy, the first comment, the per-network copies, the thread and the TEXT variables of a design (hashtags in the new",
257
+ "language; links, mentions, numbers and names kept): do not translate yourself. A design keeps its picture and the answer carries",
258
+ "needs_png=true with render {html, css, format}: render it, store the PNG with viralhunt_update_post {id, png}, then approve if wanted.",
259
+ "A plain picture (no template) stays unless `image` hands the translated one. The same translation twice is refused.",
260
+ ].join(" "),
261
+ {
262
+ id: z.number().int().describe("The source post id."),
263
+ project_id: z.number().int().describe("The target project (from translates_to on viralhunt_targets)."),
264
+ lang: z.string().optional().describe("Override the target language (es, en, pt, fr, de, it…)."),
265
+ account_ids: z.array(z.number().int()).optional().describe("Accounts of the target project (default: all postable)."),
266
+ image: z.string().optional().describe("https URL of the translated picture, for a post whose picture is not a template."),
267
+ send_mode: z.enum(["draft", "send"]).optional().describe("draft (default) waits in the target project; send (owner/admin) goes out at once when there is nothing to render."),
268
+ },
269
+ (a) => vh("/schedule.php", { method: "POST", query: { action: "translate" }, body: { id: a.id, project_id: a.project_id, lang: a.lang, account_ids: a.account_ids, image: a.image, send_mode: a.send_mode } })
270
+ );
271
+
272
+ tool(server, "viralhunt_edit_log",
273
+ "What people changed in drafts after an agent left them: one row per field (body, media, targets, scheduled_at, first_comment, overrides, thread, design) with before, after, who and when, plus by_field counts. Drafts with submitted_via=api came from an agent: those edits are what the person did not like. Read it at the start of each batch and apply the pattern (shorter captions, another picture, another hour).",
274
+ {
275
+ since: z.string().optional().describe("ISO-8601: only changes after this moment."),
276
+ id: z.number().int().optional().describe("Only one draft's changes."),
277
+ limit: z.number().int().min(1).max(500).optional(),
278
+ },
279
+ (a) => vh("/schedule.php", { query: { action: "edit_log", since: a.since, id: a.id, limit: a.limit } })
280
+ );
281
+
282
+ tool(server, "viralhunt_sync_post",
283
+ "Refresh the status of this organization's posts from the networks (call before viralhunt_get_post when the answer matters). Returns how many rows were re-read.",
284
+ {},
285
+ () => vh("/schedule.php", { method: "POST", query: { action: "sync" }, body: {} })
286
+ );
287
+
114
288
  // ── Check status ──
115
289
  tool(server, "viralhunt_get_post",
116
- "Get the current status and per-network results (incl. permalinks) of a post you created.",
290
+ [
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",
292
+ "(handed to the networks, or a target being retried), published (every target live), partial (some refused: each failed",
293
+ "results[account].error carries the network's own words and error_message sums it up), failed, draft, canceled. The app retries a",
294
+ "transient failure up to three times (results show retries, will_retry, retry_after); a final reason (dead connection, refused text,",
295
+ "oversized video) is not retried: tell the user what to fix. On a draft with a design the answer also carries render {html, css, format}.",
296
+ ].join(" "),
117
297
  { id: z.number().int().describe("The post id returned by viralhunt_schedule.") },
118
298
  (a) => vh("/schedule.php", { query: { action: "get", id: a.id } })
119
299
  );
120
300
 
121
- // ── Edit a scheduled post ──
301
+ // ── Edit a scheduled post or a draft ──
122
302
  tool(server, "viralhunt_update_post",
123
- "Edit a still-scheduled post. Only works while status is 'scheduled' and >5 min before publish. Only the fields you send change; the project cannot be changed. Check status with viralhunt_get_post first; on a 409 error re-fetch instead of retrying.",
303
+ [
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);",
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",
307
+ "edited only while status is 'scheduled' and more than 5 min before publish. Only the fields you send change; the project cannot",
308
+ "be changed. Check status with viralhunt_get_post first; on a 409 error re-fetch instead of retrying.",
309
+ ].join(" "),
124
310
  {
125
311
  id: z.number().int().describe("The post id."),
126
312
  body: z.string().optional().describe("New caption."),
@@ -128,12 +314,112 @@ tool(server, "viralhunt_update_post",
128
314
  networks: z.array(z.string()).optional().describe("New network set (adds/removes targets)."),
129
315
  target_account_ids: z.array(z.number().int()).optional().describe("New exact account-id set."),
130
316
  scheduled_at: z.string().optional().describe("New ISO-8601 UTC time (must be in the future)."),
317
+ first_comment: z.string().optional().describe("Drafts: the first comment."),
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."),
321
+ design: z.object({}).passthrough().optional().describe("Drafts: the filled template (see viralhunt_schedule)."),
322
+ png: z.string().optional().describe("Drafts: data:image/png;base64,… of the rendered design; stored and made the post's first picture."),
131
323
  },
132
324
  (a) => vh("/schedule.php", { method: "POST", query: { action: "update" }, body: {
133
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,
326
+ first_comment: a.first_comment, author_notes: a.author_notes, kind: a.kind, overrides: a.overrides, design: a.design, png: a.png,
134
327
  } })
135
328
  );
136
329
 
330
+ // ── What performed ──
331
+ tool(server, "viralhunt_stats",
332
+ "Engagement of the published posts as the networks report it back: by_network, by_project and top_posts (with permalinks). ready=false means nothing collected yet. Use it to answer 'what worked' and to find more like the winners (viralhunt_search on the winning topic); never invent a number.",
333
+ {
334
+ project: z.string().optional(),
335
+ project_id: z.number().int().optional(),
336
+ network: z.string().optional(),
337
+ days: z.number().int().min(1).max(365).optional().describe("Window in days (default 30)."),
338
+ limit: z.number().int().min(1).max(100).optional(),
339
+ },
340
+ (a) => vh("/stats.php", { query: { project: a.project, project_id: a.project_id, network: a.network, days: a.days, limit: a.limit } })
341
+ );
342
+
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
+
371
+ tool(server, "viralhunt_quotes",
372
+ [
373
+ "Quotes ranked by our popularity score, for daily-quote series: text, author, author_context (description, born, died), work,",
374
+ "categories, rights, popularity high|medium|low and author_image (portrait url, licence, credit). Default rights=public_domain",
375
+ "(authors dead more than 70 years): keep it unless the user asks for a modern author, and with rights=any tell the user a quote is",
376
+ "restricted before scheduling it under their brand. Write the caption's context FROM author_context and work, never from memory.",
377
+ "categories_list=true lists the categories held. unused=true leaves out what this organization already used.",
378
+ ].join(" "),
379
+ {
380
+ category: z.string().optional().describe("One category, e.g. stoicism."),
381
+ categories: z.string().optional().describe("Several mixed: stoicism,leadership."),
382
+ lang: z.enum(["en", "es"]).optional().describe("Language of the quote (default en)."),
383
+ max_chars: z.number().int().min(20).max(600).optional().describe("Default 180: what the quote templates fit."),
384
+ rights: z.enum(["public_domain", "any"]).optional(),
385
+ author: z.string().optional().describe("Part of an author's name."),
386
+ sort: z.enum(["score", "random"]).optional(),
387
+ unused: z.boolean().optional(),
388
+ categories_list: z.boolean().optional().describe("true = list the categories instead of quotes."),
389
+ per_page: z.number().int().min(1).max(50).optional(),
390
+ page: z.number().int().min(1).optional(),
391
+ },
392
+ (a) => vh("/quotes.php", { query: { category: a.category, categories: a.categories, lang: a.lang, max_chars: a.max_chars, rights: a.rights, author: a.author, sort: a.sort, unused: a.unused ? 1 : undefined, categories_list: a.categories_list ? 1 : undefined, per_page: a.per_page, page: a.page } })
393
+ );
394
+
395
+ tool(server, "viralhunt_mark_quote_used",
396
+ "After scheduling a quote, mark it used for this organization so the next pick (unused=true) is a different one.",
397
+ { quote_id: z.number().int(), note: z.string().optional().describe("e.g. 'scheduled 2026-09-20 09:00 instagram'.") },
398
+ (a) => vh("/quotes.php", { method: "POST", body: { action: "mark_used", quote_id: a.quote_id, note: a.note } })
399
+ );
400
+
401
+ // ── Recipes: standing orders a person saved in the app ──
402
+ tool(server, "viralhunt_recipes",
403
+ [
404
+ "The standing orders of a brand, saved by a person in the app: each recipe says what to post from (source quotes|trending|manual with",
405
+ "source_params, and source_call = the exact request that fetches the content), which template (template.slug for viralhunt_get_template),",
406
+ "format, networks, cadence (daily|weekdays|weekly|manual), post_time in the project's timezone (null = the network's best time),",
407
+ "caption_brief and last_run_at. Nothing runs on the server: you do the loop (fetch, fill, render, schedule, mark the quote used,",
408
+ "then viralhunt_recipe_ran). daily with last_run_at older than today = due. Never run a paused recipe (is_active false).",
409
+ ].join(" "),
410
+ {
411
+ project_id: z.number().int().optional().describe("One brand (default: every active recipe of the organization)."),
412
+ active: z.boolean().optional(),
413
+ },
414
+ (a) => vh("/recipes.php", { query: { project_id: a.project_id, active: a.active === false ? 0 : 1 } })
415
+ );
416
+
417
+ tool(server, "viralhunt_recipe_ran",
418
+ "Stamp a recipe run so the person sees in the app that it happened.",
419
+ { recipe_id: z.number().int(), note: z.string().optional() },
420
+ (a) => vh("/recipes.php", { method: "POST", body: { action: "ran", recipe_id: a.recipe_id, note: a.note } })
421
+ );
422
+
137
423
  // ── Content templates: browse the library ──
138
424
  tool(server, "viralhunt_list_templates",
139
425
  "List ViralHunt content templates (layouts you fill and render yourself). Lean by design — no html/css — so use viralhunt_get_template for the one you'll actually render.",
@@ -144,13 +430,20 @@ tool(server, "viralhunt_list_templates",
144
430
  project_id: z.number().int().optional().describe("With assigned=true, the project whose allowed templates you want."),
145
431
  assigned: z.boolean().optional().describe("true = only the templates assigned to project_id."),
146
432
  q: z.string().optional().describe("Search name/description."),
433
+ favorites: z.boolean().optional().describe("true = only the templates the brand starred. Favourites come first in every list: prefer them when several fit."),
147
434
  },
148
- (a) => vh("/templates.php", { query: { category: a.category, media_type: a.media_type, network: a.network, q: a.q, assigned: a.assigned ? 1 : undefined, project_id: a.project_id } })
435
+ (a) => vh("/templates.php", { query: { category: a.category, media_type: a.media_type, network: a.network, q: a.q, assigned: a.assigned ? 1 : undefined, project_id: a.project_id, favorites: a.favorites ? 1 : undefined } })
149
436
  );
150
437
 
151
438
  // ── Content templates: the full layout to render ──
152
439
  tool(server, "viralhunt_get_template",
153
- "Get one template's full spec: html, css, the variable manifest (with hard rules per variable), formats, palette tokens, embedded fonts and render_tech. Fill every {{variable}}, then render with headless Chrome at the chosen format's size and WAIT for [data-vh-ready='1'] before screenshotting .vh-card.",
440
+ [
441
+ "Get one template's full spec: html, css, the variable manifest (with hard rules per variable), formats, palette tokens, embedded fonts,",
442
+ "render_tech, plus what is `static` (the owner fixed it: logo, frame, signature, fixed texts; never move, cover or restyle it), what is",
443
+ "`dynamic` (the variables you fill: the picture, the headline, the caption that is the post body), what it `suits` (news, photo, quote,",
444
+ "number, tip, on_this_day, meme, any) and the owner's `instructions` (they override any general rule). Fill every dynamic {{variable}},",
445
+ "then render with headless Chrome at the chosen format's size and WAIT for [data-vh-ready='1'] before screenshotting .vh-card.",
446
+ ].join(" "),
154
447
  {
155
448
  slug: z.string().optional().describe("Template slug, e.g. vh-image-card."),
156
449
  id: z.number().int().optional().describe("Template id — alternative to slug."),
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.2.1",
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; learn the best time to post per network from measured viral posts; get top hashtags, trending sounds and the best subreddits or Bluesky feeds for a topic; and schedule/publish/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"