rankcontrol 0.8.3 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -12
- package/package.json +4 -1
- package/src/cli.mjs +73 -18
- package/src/client.mjs +15 -3
- package/src/login.mjs +0 -3
- package/src/mcp.mjs +99 -29
package/README.md
CHANGED
|
@@ -39,11 +39,12 @@ npx rankcontrol citations --model chatgpt
|
|
|
39
39
|
npx rankcontrol crawler-access # can AI crawlers reach your site (daily probe)
|
|
40
40
|
npx rankcontrol queries # tracked queries checked weekly across AI engines
|
|
41
41
|
npx rankcontrol query-add "best crm for agencies"
|
|
42
|
+
npx rankcontrol query-add "Do luggage trackers work abroad?" --covered-by <contentId> # an article already answers it
|
|
42
43
|
npx rankcontrol query-edit <queryId> "new text"
|
|
43
44
|
npx rankcontrol query-track <queryId> off # pause weekly checks (frees a slot)
|
|
44
45
|
npx rankcontrol query-remove <queryId>
|
|
45
46
|
npx rankcontrol competitors
|
|
46
|
-
npx rankcontrol competitor-add "Acme"
|
|
47
|
+
npx rankcontrol competitor-add "Acme" # site resolved from the name; pass a URL to override
|
|
47
48
|
npx rankcontrol competitor-remove <competitorId>
|
|
48
49
|
|
|
49
50
|
# Content pipeline
|
|
@@ -52,8 +53,9 @@ npx rankcontrol content # pages with status
|
|
|
52
53
|
npx rankcontrol plan-content --topics "bus travel" --max-difficulty 40
|
|
53
54
|
npx rankcontrol commit-titles --file titles.json
|
|
54
55
|
npx rankcontrol ideas # scored content-idea backlog
|
|
56
|
+
npx rankcontrol idea-hide <queryId> # hide an idea; a tracked query keeps its checks
|
|
55
57
|
npx rankcontrol plan-idea "query text" # put an idea on the calendar
|
|
56
|
-
npx rankcontrol delete-planned <contentId> #
|
|
58
|
+
npx rankcontrol delete-planned <contentId> # its keyword returns to Content Ideas
|
|
57
59
|
npx rankcontrol generate <contentId> --confirm # write a planned article now
|
|
58
60
|
npx rankcontrol publish <contentId> --confirm # dry run without --confirm
|
|
59
61
|
npx rankcontrol archive <contentId>
|
|
@@ -61,14 +63,15 @@ npx rankcontrol reschedule <contentId> 2026-08-04
|
|
|
61
63
|
npx rankcontrol settings # article policy (auto-write, auto-publish, images, ...)
|
|
62
64
|
npx rankcontrol settings-set --auto-generate off --auto-publish off
|
|
63
65
|
npx rankcontrol topics # pillar list (topic clusters)
|
|
64
|
-
npx rankcontrol topics-set "Topic A" "Topic B" # replaces the FULL list
|
|
66
|
+
npx rankcontrol topics-set "Topic A" "Topic B" # replaces the FULL list; up to 20 topics
|
|
65
67
|
npx rankcontrol internal-links <contentId> # who links here / links out
|
|
66
68
|
npx rankcontrol site-pages # pages used for internal links
|
|
67
69
|
npx rankcontrol detect-links https://example.com/sitemap.xml
|
|
68
70
|
npx rankcontrol add-pages https://example.com/pricing
|
|
69
71
|
|
|
70
|
-
#
|
|
72
|
+
# Analytics
|
|
71
73
|
npx rankcontrol traffic --days 30 # page views, visitors, bounce rate, time on page
|
|
74
|
+
npx rankcontrol rankings # Verified Google + Bing positions, GSC clicks/impressions/average, AI Overview rate per keyword (--scope ours|site)
|
|
72
75
|
npx rankcontrol engagement # per-page views, citations, sparkline
|
|
73
76
|
npx rankcontrol analytics-sources # which source writes traffic/crawler data
|
|
74
77
|
npx rankcontrol analytics-set-source <dataType> <source>
|
|
@@ -96,13 +99,15 @@ npx rankcontrol network-remove-placement <placementId> --confirm
|
|
|
96
99
|
npx rankcontrol repurpose [contentId] # published articles + social drafts (8 platforms)
|
|
97
100
|
npx rankcontrol repurpose-generate <contentId> --platforms twitter --confirm
|
|
98
101
|
npx rankcontrol repurpose-edit <draftId> --title "New hook"
|
|
99
|
-
npx rankcontrol repurpose-channels # Postiz channel ids
|
|
100
|
-
npx rankcontrol repurpose-push <draftId> --channels <id> --confirm
|
|
102
|
+
npx rankcontrol repurpose-channels # Postiz and Buffer channel ids
|
|
103
|
+
npx rankcontrol repurpose-push <draftId> --channels <id> --confirm # --scheduler postiz|buffer when both are connected
|
|
101
104
|
npx rankcontrol repurpose-mark-posted <draftId>
|
|
102
105
|
npx rankcontrol social # Reddit/X threads where your articles fit
|
|
103
106
|
npx rankcontrol social-stats
|
|
104
|
-
npx rankcontrol social-status <threadId>
|
|
107
|
+
npx rankcontrol social-status <threadId> skipped
|
|
105
108
|
npx rankcontrol social-draft-reply <threadId>
|
|
109
|
+
npx rankcontrol social-open <threadId> [--text "<edited reply>"]
|
|
110
|
+
npx rankcontrol social-x-handle <handle|clear>
|
|
106
111
|
|
|
107
112
|
# Brand, team, workspace
|
|
108
113
|
npx rankcontrol brand # brand profile, products, ICPs in one read
|
|
@@ -110,6 +115,8 @@ npx rankcontrol brand-set --industry "SaaS" # name, industry, description, au
|
|
|
110
115
|
npx rankcontrol brand-profile-set --json '{"tone":"confident"}'
|
|
111
116
|
npx rankcontrol brand-product --json @product.json
|
|
112
117
|
npx rankcontrol brand-icp --json @icp.json
|
|
118
|
+
npx rankcontrol icp-suggest # up to 8 audiences for this site; saves nothing
|
|
119
|
+
npx rankcontrol icp-draft --describe "Ops leads at 50-500 person logistics firms" # one profile to review; save with brand-icp
|
|
113
120
|
npx rankcontrol team
|
|
114
121
|
npx rankcontrol team-invite user@company.com --perms content=write,analytics=read --confirm
|
|
115
122
|
npx rankcontrol team-revoke <invitationId>
|
|
@@ -147,7 +154,7 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
147
154
|
}
|
|
148
155
|
```
|
|
149
156
|
|
|
150
|
-
### Tools (
|
|
157
|
+
### Tools (80+)
|
|
151
158
|
|
|
152
159
|
**Visibility and citations**
|
|
153
160
|
|
|
@@ -164,6 +171,7 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
164
171
|
| `get_crawler_access` | Daily AI-crawler reachability probe (edge blocks, robots.txt) |
|
|
165
172
|
| `list_tracked_queries` | The queries checked weekly across the AI engines |
|
|
166
173
|
| `add_tracked_query` / `update_tracked_query` / `set_query_tracking` / `remove_tracked_query` | Manage the query pool and its plan slots |
|
|
174
|
+
| `hide_content_idea` | Hide an idea from Content Ideas; a tracked query keeps its weekly checks and its slot |
|
|
167
175
|
| `get_competitors` / `add_competitor` / `remove_competitor` | Manage share-of-voice competitors (max 10) |
|
|
168
176
|
|
|
169
177
|
**Content pipeline**
|
|
@@ -180,16 +188,17 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
180
188
|
| `archive_content` | Archive an article (reversible in the dashboard) |
|
|
181
189
|
| `reschedule_article` | Move a planned article to another day (flexible schedule) |
|
|
182
190
|
| `get_article_settings` / `update_article_settings` | Read / merge-patch the article policy |
|
|
183
|
-
| `get_topics` / `set_topics` | Read / replace the pillar list (full-list replace) |
|
|
191
|
+
| `get_topics` / `set_topics` | Read / replace the pillar list (full-list replace, up to 20 topics) |
|
|
184
192
|
| `get_internal_links` | Who links to this article internally, and its outbound count |
|
|
185
193
|
| `list_site_pages` / `add_site_pages` / `detect_site_links` | Pages used for in-article internal links |
|
|
186
194
|
| `list_content` | Content pages with status |
|
|
187
195
|
|
|
188
|
-
**
|
|
196
|
+
**Analytics**
|
|
189
197
|
|
|
190
198
|
| Tool | What it does |
|
|
191
199
|
|---|---|
|
|
192
200
|
| `get_traffic_overview` | Page views, visitors, sessions, bounce rate, time on page |
|
|
201
|
+
| `get_rankings` | Verified Google and Bing positions per keyword, Search Console clicks, impressions and average position (never shown as a rank), which page earned each query and whether it is yours, how often Google's AI Overview shows and cites you; `scope` ours (default) or site |
|
|
193
202
|
| `get_page_engagement` | Per-page views, citations, and a view sparkline |
|
|
194
203
|
| `get_analytics_sources` / `set_analytics_source` | Which source writes traffic and crawler data |
|
|
195
204
|
| `activate_analytics_screen` | One-time activation of the Analytics or Reports screen |
|
|
@@ -222,10 +231,10 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
222
231
|
|---|---|
|
|
223
232
|
| `list_repurpose_queue` / `get_repurpose_drafts` | Published articles + social drafts (8 platforms) |
|
|
224
233
|
| `generate_repurpose_drafts` / `update_repurpose_draft` | Draft and edit platform-native posts |
|
|
225
|
-
| `list_postiz_channels` / `push_repurpose_draft` | Push to Postiz; dry run by default |
|
|
234
|
+
| `list_postiz_channels` / `push_repurpose_draft` | Push to Buffer or Postiz; dry run by default |
|
|
226
235
|
| `mark_repurpose_posted` | Record a draft as posted manually |
|
|
227
236
|
| `list_social_threads` / `get_social_stats` | Reddit/X threads where your articles fit |
|
|
228
|
-
| `update_social_thread_status` / `draft_social_reply` | Work a thread;
|
|
237
|
+
| `update_social_thread_status` / `draft_social_reply` / `open_social_thread` / `set_social_x_handle` | Work a thread: draft, then open it to post yourself (X gets a pre-filled composer link); the thread marks itself Posted once your reply is found. Drafts never auto-post; Posted is never set by hand |
|
|
229
238
|
|
|
230
239
|
**Brand, team, workspace**
|
|
231
240
|
|
|
@@ -235,6 +244,7 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
235
244
|
| `update_brand_identity` | Name, industry, description, authors (EEAT bylines), style refs |
|
|
236
245
|
| `update_brand_profile` | Voice, colors, meta fields, locale |
|
|
237
246
|
| `write_brand_product` / `write_brand_icp` | Create/update/delete products and buyer profiles |
|
|
247
|
+
| `suggest_brand_icps` / `draft_brand_icp` | Suggest audiences for this site and draft one buyer profile to review; nothing is saved until `write_brand_icp` |
|
|
238
248
|
| `get_team` / `invite_team_member` / `revoke_team_invite` / `remove_team_member` | Seats, invites, per-screen permissions |
|
|
239
249
|
| `contact_support` | Message the RankControl team with the workspace identified |
|
|
240
250
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rankcontrol",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "RankControl CLI + MCP server: drive your SEO/AI-visibility workspace from the terminal or any AI agent",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"prepublishOnly": "node ../../scripts/check-package-parity.mjs"
|
|
8
|
+
},
|
|
6
9
|
"homepage": "https://rctrl.com",
|
|
7
10
|
"type": "module",
|
|
8
11
|
"bin": {
|
package/src/cli.mjs
CHANGED
|
@@ -58,6 +58,12 @@ export function runCli(argv) {
|
|
|
58
58
|
.option("--days <n>", "7, 30 or 90", "30")
|
|
59
59
|
.action((opts) => api.trafficOverview(Number(opts.days)).then(out).catch(fail));
|
|
60
60
|
|
|
61
|
+
program
|
|
62
|
+
.command("rankings")
|
|
63
|
+
.description("Verified Google and Bing positions per keyword, Search Console clicks, impressions and average position when connected (an average, never a rank), which page earned each query, and how often Google's AI Overview shows above it and cites you")
|
|
64
|
+
.option("--scope <scope>", '"ours" (default): queries your published articles appear for; "site": everything on the domain', "ours")
|
|
65
|
+
.action((opts) => api.rankings(opts.scope).then(out).catch(fail));
|
|
66
|
+
|
|
61
67
|
program
|
|
62
68
|
.command("score")
|
|
63
69
|
.description("Composite visibility score blending AI citation rate with Google and Bing rank share, with subscores")
|
|
@@ -198,19 +204,26 @@ export function runCli(argv) {
|
|
|
198
204
|
|
|
199
205
|
program
|
|
200
206
|
.command("query-add")
|
|
201
|
-
.description("Add a query
|
|
207
|
+
.description("Add a query (tracks immediately when a tracking slot is free)")
|
|
202
208
|
.argument("<queryText>", "The search/AI query to track")
|
|
203
|
-
.
|
|
209
|
+
.option("--covered-by <contentId>", "Article that already answers this query, so Content Ideas skips it")
|
|
210
|
+
.action((queryText, opts) => api.addQuery(queryText, opts.coveredBy).then(out).catch(fail));
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command("idea-hide")
|
|
214
|
+
.description("Hide an idea from Content Ideas. A tracked query keeps its weekly checks")
|
|
215
|
+
.argument("<queryId>", "Idea id (from the ideas command)")
|
|
216
|
+
.action((queryId) => api.hideIdea(queryId).then(out).catch(fail));
|
|
204
217
|
|
|
205
218
|
program
|
|
206
219
|
.command("query-remove")
|
|
207
|
-
.description("Delete a query
|
|
220
|
+
.description("Delete a query")
|
|
208
221
|
.argument("<queryId>", "Query id (from the queries command)")
|
|
209
222
|
.action((queryId) => api.removeQuery(queryId).then(out).catch(fail));
|
|
210
223
|
|
|
211
224
|
program
|
|
212
225
|
.command("query-track")
|
|
213
|
-
.description("Pause or resume weekly checks on a query. Tracking uses
|
|
226
|
+
.description("Pause or resume weekly checks on a query. Tracking uses one of your tracking slots; pausing frees it")
|
|
214
227
|
.argument("<queryId>", "Query id (from the queries command)")
|
|
215
228
|
.argument("<state>", "on | off")
|
|
216
229
|
.action((queryId, state) =>
|
|
@@ -228,14 +241,14 @@ export function runCli(argv) {
|
|
|
228
241
|
|
|
229
242
|
program
|
|
230
243
|
.command("competitors")
|
|
231
|
-
.description("Tracked competitors used in share-of-voice and visibility comparisons")
|
|
244
|
+
.description("Tracked competitors used in share-of-voice and visibility comparisons, with each one's AI visibility score, citations of checks over the last 30 days, domain metrics and positioning analysis")
|
|
232
245
|
.action(() => api.competitors().then(out).catch(fail));
|
|
233
246
|
|
|
234
247
|
program
|
|
235
248
|
.command("competitor-add")
|
|
236
|
-
.description("Track a competitor (max 10)")
|
|
249
|
+
.description("Track a competitor (max 10); the site is resolved from the name when omitted")
|
|
237
250
|
.argument("<name>", "Competitor name")
|
|
238
|
-
.argument("
|
|
251
|
+
.argument("[websiteUrl]", "Competitor website URL (optional)")
|
|
239
252
|
.action((name, websiteUrl) =>
|
|
240
253
|
api.addCompetitor(name, websiteUrl).then(out).catch(fail)
|
|
241
254
|
);
|
|
@@ -255,7 +268,7 @@ export function runCli(argv) {
|
|
|
255
268
|
|
|
256
269
|
program
|
|
257
270
|
.command("topics-set")
|
|
258
|
-
.description("Replace the full pillar list (create/dedupe/safe-delete reconcile)")
|
|
271
|
+
.description("Replace the full pillar list (create/dedupe/safe-delete reconcile); up to 20 topics")
|
|
259
272
|
.argument("<topics...>", "Topic names, space-separated (quote multi-word topics)")
|
|
260
273
|
.action((topics) => api.setTopics(topics).then(out).catch(fail));
|
|
261
274
|
|
|
@@ -492,11 +505,10 @@ export function runCli(argv) {
|
|
|
492
505
|
|
|
493
506
|
program
|
|
494
507
|
.command("reschedule <contentId> <date>")
|
|
495
|
-
.description("Move a planned article to a day (YYYY-MM-DD
|
|
508
|
+
.description("Move a planned article to a day (YYYY-MM-DD in the workspace's timezone; needs the flexible schedule)")
|
|
496
509
|
.action((contentId, date) => {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
return api.reschedule(contentId, day.getTime()).then(out).catch(fail);
|
|
510
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return fail(new Error("Date must be YYYY-MM-DD"));
|
|
511
|
+
return api.reschedule(contentId, date).then(out).catch(fail);
|
|
500
512
|
});
|
|
501
513
|
|
|
502
514
|
program
|
|
@@ -536,7 +548,7 @@ export function runCli(argv) {
|
|
|
536
548
|
|
|
537
549
|
program
|
|
538
550
|
.command("repurpose-generate <contentId>")
|
|
539
|
-
.description("Draft social posts for a published article (dry run unless --confirm; costly)")
|
|
551
|
+
.description("Draft social posts for a published article that has none yet (dry run unless --confirm; costly)")
|
|
540
552
|
.option(
|
|
541
553
|
"--platforms <a,b>",
|
|
542
554
|
"Subset: linkedin,twitter,pinterest,instagram,facebook,threads,youtube,tiktok"
|
|
@@ -572,20 +584,22 @@ export function runCli(argv) {
|
|
|
572
584
|
|
|
573
585
|
program
|
|
574
586
|
.command("repurpose-channels")
|
|
575
|
-
.description("Connected Postiz channels (ids needed for repurpose-push)")
|
|
587
|
+
.description("Connected Postiz and Buffer channels (ids needed for repurpose-push)")
|
|
576
588
|
.action(() => api.repurposeChannels().then(out).catch(fail));
|
|
577
589
|
|
|
578
590
|
program
|
|
579
591
|
.command("repurpose-push <draftId>")
|
|
580
|
-
.description("Send a draft to Postiz (dry run unless --confirm)")
|
|
581
|
-
.option("--
|
|
582
|
-
.option("--
|
|
592
|
+
.description("Send a draft to Postiz or Buffer (dry run unless --confirm)")
|
|
593
|
+
.option("--scheduler <postiz|buffer>", "Which scheduler (required when both are connected)")
|
|
594
|
+
.option("--channels <id1,id2>", "Channel ids (see repurpose-channels)")
|
|
595
|
+
.option("--when <now|schedule|draft|queue>", "Mode; queue is Buffer only", "now")
|
|
583
596
|
.option("--date <iso>", "ISO time for --when schedule")
|
|
584
597
|
.option("--confirm", "Actually push (publicly visible)")
|
|
585
598
|
.action((draftId, opts) =>
|
|
586
599
|
api
|
|
587
600
|
.repurposePush({
|
|
588
601
|
draftId,
|
|
602
|
+
scheduler: opts.scheduler,
|
|
589
603
|
integrationIds: opts.channels ? list(opts.channels) : undefined,
|
|
590
604
|
scheduleType: opts.when,
|
|
591
605
|
date: opts.date,
|
|
@@ -707,11 +721,28 @@ export function runCli(argv) {
|
|
|
707
721
|
|
|
708
722
|
program
|
|
709
723
|
.command("social-status <threadId> <status>")
|
|
710
|
-
.description("
|
|
724
|
+
.description("Skip a thread (the only manual status; Posted is detected automatically)")
|
|
711
725
|
.action((threadId, status) =>
|
|
712
726
|
api.socialStatus(threadId, status).then(out).catch(fail)
|
|
713
727
|
);
|
|
714
728
|
|
|
729
|
+
program
|
|
730
|
+
.command("social-open <threadId>")
|
|
731
|
+
.description(
|
|
732
|
+
"Take a drafted reply to the platform yourself: prints the thread URL (for X, a composer link with the reply filled in) and starts the checks that mark it Posted"
|
|
733
|
+
)
|
|
734
|
+
.option("--text <reply>", "Use an edited reply (X: 280 characters or fewer)")
|
|
735
|
+
.action((threadId, opts) =>
|
|
736
|
+
api.socialOpen(threadId, opts.text).then(out).catch(fail)
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
program
|
|
740
|
+
.command("social-x-handle <handle>")
|
|
741
|
+
.description("Save the X handle used to recognise your posted replies (\"clear\" removes it)")
|
|
742
|
+
.action((handle) =>
|
|
743
|
+
api.socialXHandle(handle === "clear" ? "" : handle).then(out).catch(fail)
|
|
744
|
+
);
|
|
745
|
+
|
|
715
746
|
program
|
|
716
747
|
.command("social-draft-reply <threadId>")
|
|
717
748
|
.description("AI-draft a reply for a thread (nothing posts; the draft lands for review)")
|
|
@@ -773,5 +804,29 @@ export function runCli(argv) {
|
|
|
773
804
|
api.brandIcpWrite(parseJson(opts.json, "brand-icp")).then(out).catch(fail)
|
|
774
805
|
);
|
|
775
806
|
|
|
807
|
+
program
|
|
808
|
+
.command("icp-suggest")
|
|
809
|
+
.description("Suggest up to 8 audiences for this site from its brand, products, competitors and topics (saves nothing)")
|
|
810
|
+
.option("--exclude <titles...>", "Audience titles already shown, to get different ones")
|
|
811
|
+
.action((opts) => api.suggestIcps(opts.exclude ?? []).then(out).catch(fail));
|
|
812
|
+
|
|
813
|
+
program
|
|
814
|
+
.command("icp-draft")
|
|
815
|
+
.description("Draft one buyer profile from a sentence or a suggestion (saves nothing; save it with brand-icp)")
|
|
816
|
+
.option("--describe <sentence>", "Who the audience is, in one sentence")
|
|
817
|
+
.option("--title <title>", "Title of a suggestion from icp-suggest")
|
|
818
|
+
.option("--who <sentence>", "The suggestion's one-line description")
|
|
819
|
+
.option("--json <json>", "Fields to keep as written, JSON (or @file.json); a key you pass is kept, a key you leave out is filled")
|
|
820
|
+
.action((opts) =>
|
|
821
|
+
api
|
|
822
|
+
.draftIcp({
|
|
823
|
+
describe: opts.describe,
|
|
824
|
+
suggestion: opts.title ? { title: opts.title, who: opts.who } : undefined,
|
|
825
|
+
given: opts.json ? parseJson(opts.json, "icp-draft") : undefined,
|
|
826
|
+
})
|
|
827
|
+
.then(out)
|
|
828
|
+
.catch(fail)
|
|
829
|
+
);
|
|
830
|
+
|
|
776
831
|
program.parseAsync(argv);
|
|
777
832
|
}
|
package/src/client.mjs
CHANGED
|
@@ -124,8 +124,8 @@ export const api = {
|
|
|
124
124
|
articleSettings: () => request("GET", "/api/v1/settings/articles"),
|
|
125
125
|
updateArticleSettings: (opts) =>
|
|
126
126
|
request("POST", "/api/v1/settings/articles", opts),
|
|
127
|
-
reschedule: (contentId,
|
|
128
|
-
request("POST", "/api/v1/content/reschedule", { contentId,
|
|
127
|
+
reschedule: (contentId, date) =>
|
|
128
|
+
request("POST", "/api/v1/content/reschedule", { contentId, date }),
|
|
129
129
|
sitePages: () => request("GET", "/api/v1/links/pages"),
|
|
130
130
|
detectSiteLinks: (source, url) =>
|
|
131
131
|
request("POST", "/api/v1/links/detect", { source, url }),
|
|
@@ -144,9 +144,12 @@ export const api = {
|
|
|
144
144
|
request("POST", "/api/v1/repurpose/mark-posted", { draftId }),
|
|
145
145
|
repurposePush: (opts) => request("POST", "/api/v1/repurpose/push", opts),
|
|
146
146
|
trackedQueries: () => request("GET", "/api/v1/queries"),
|
|
147
|
-
addQuery: (queryText) =>
|
|
147
|
+
addQuery: (queryText, coveredByContentId) =>
|
|
148
|
+
request("POST", "/api/v1/queries", coveredByContentId ? { queryText, coveredByContentId } : { queryText }),
|
|
148
149
|
removeQuery: (queryId) =>
|
|
149
150
|
request("POST", "/api/v1/queries/remove", { queryId }),
|
|
151
|
+
hideIdea: (queryId) =>
|
|
152
|
+
request("POST", "/api/v1/content/ideas/hide", { queryId }),
|
|
150
153
|
setQueryTracking: (queryId, tracked) =>
|
|
151
154
|
request("POST", "/api/v1/queries/set-tracking", { queryId, tracked }),
|
|
152
155
|
updateQuery: (queryId, queryText) =>
|
|
@@ -163,6 +166,8 @@ export const api = {
|
|
|
163
166
|
crawlerAccess: () => request("GET", "/api/v1/crawler-access"),
|
|
164
167
|
trafficOverview: (days = 30) =>
|
|
165
168
|
request("GET", `/api/v1/analytics/traffic?days=${days}`),
|
|
169
|
+
rankings: (scope = "ours") =>
|
|
170
|
+
request("GET", `/api/v1/analytics/rankings?scope=${scope === "site" ? "site" : "ours"}`),
|
|
166
171
|
analyticsSources: () => request("GET", "/api/v1/analytics/sources"),
|
|
167
172
|
setAnalyticsSource: (dataType, source) =>
|
|
168
173
|
request("POST", "/api/v1/analytics/sources", { dataType, source }),
|
|
@@ -232,10 +237,17 @@ export const api = {
|
|
|
232
237
|
request("POST", "/api/v1/social/status", { threadId, status }),
|
|
233
238
|
socialDraftReply: (threadId, mentionMode) =>
|
|
234
239
|
request("POST", "/api/v1/social/draft-reply", { threadId, mentionMode }),
|
|
240
|
+
socialOpen: (threadId, text) =>
|
|
241
|
+
request("POST", "/api/v1/social/open", { threadId, text }),
|
|
242
|
+
socialXHandle: (handle) =>
|
|
243
|
+
request("POST", "/api/v1/social/x-handle", { handle }),
|
|
235
244
|
support: (opts) => request("POST", "/api/v1/support", opts),
|
|
236
245
|
brand: () => request("GET", "/api/v1/brand"),
|
|
237
246
|
brandIdentitySet: (opts) => request("POST", "/api/v1/brand", opts),
|
|
238
247
|
brandProfileSet: (opts) => request("POST", "/api/v1/brand/profile", opts),
|
|
239
248
|
brandProductWrite: (opts) => request("POST", "/api/v1/brand/product", opts),
|
|
240
249
|
brandIcpWrite: (opts) => request("POST", "/api/v1/brand/icp", opts),
|
|
250
|
+
suggestIcps: (exclude = []) =>
|
|
251
|
+
request("POST", "/api/v1/brand/icp/suggest", { exclude }),
|
|
252
|
+
draftIcp: (opts) => request("POST", "/api/v1/brand/icp/draft", opts),
|
|
241
253
|
};
|
package/src/login.mjs
CHANGED
|
@@ -13,13 +13,10 @@ const DEFAULT_SCOPES = [
|
|
|
13
13
|
"read:content",
|
|
14
14
|
"read:analytics",
|
|
15
15
|
"write:content",
|
|
16
|
-
"write:queries",
|
|
17
16
|
"write:backlinks",
|
|
18
17
|
"write:social",
|
|
19
18
|
"write:publish",
|
|
20
|
-
"manage:integrations",
|
|
21
19
|
"manage:org",
|
|
22
|
-
"trigger:agents",
|
|
23
20
|
];
|
|
24
21
|
|
|
25
22
|
function openBrowser(url) {
|
package/src/mcp.mjs
CHANGED
|
@@ -29,7 +29,7 @@ const plannedTitle = z.object({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
export async function startMcpServer() {
|
|
32
|
-
const server = new McpServer({ name: "rankcontrol", version: "0.
|
|
32
|
+
const server = new McpServer({ name: "rankcontrol", version: "0.13.0" });
|
|
33
33
|
|
|
34
34
|
server.tool(
|
|
35
35
|
"get_overview_funnel",
|
|
@@ -195,21 +195,34 @@ export async function startMcpServer() {
|
|
|
195
195
|
|
|
196
196
|
server.tool(
|
|
197
197
|
"add_tracked_query",
|
|
198
|
-
"Add a query
|
|
199
|
-
{
|
|
200
|
-
|
|
198
|
+
"Add a query. Weekly AI citation checks start immediately when a tracking slot is free; otherwise it is saved untracked. Pass coveredByContentId when the query is a question an existing article already answers, so Content Ideas does not offer it again.",
|
|
199
|
+
{
|
|
200
|
+
queryText: z.string().describe("The search/AI query to track"),
|
|
201
|
+
coveredByContentId: z
|
|
202
|
+
.string()
|
|
203
|
+
.optional()
|
|
204
|
+
.describe("contentPage id of the article that already answers this query"),
|
|
205
|
+
},
|
|
206
|
+
run(({ queryText, coveredByContentId }) => api.addQuery(queryText, coveredByContentId))
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
server.tool(
|
|
210
|
+
"hide_content_idea",
|
|
211
|
+
"Hide an idea from Content Ideas. A tracked query keeps its weekly checks and its slot; use remove_tracked_query to delete a query outright.",
|
|
212
|
+
{ queryId: z.string().describe("Idea id from get_content_ideas") },
|
|
213
|
+
run(({ queryId }) => api.hideIdea(queryId))
|
|
201
214
|
);
|
|
202
215
|
|
|
203
216
|
server.tool(
|
|
204
217
|
"remove_tracked_query",
|
|
205
|
-
"Delete a query
|
|
218
|
+
"Delete a query. Frees its tracking slot if it was tracked.",
|
|
206
219
|
{ queryId: z.string().describe("Query id from get_tracked_queries") },
|
|
207
220
|
run(({ queryId }) => api.removeQuery(queryId))
|
|
208
221
|
);
|
|
209
222
|
|
|
210
223
|
server.tool(
|
|
211
224
|
"set_query_tracking",
|
|
212
|
-
"Pause or resume weekly AI citation checks on a
|
|
225
|
+
"Pause or resume weekly AI citation checks on a saved query. Tracking uses one of the plan's tracking slots; pausing frees it. The query is kept either way.",
|
|
213
226
|
{
|
|
214
227
|
queryId: z.string().describe("Query id from get_tracked_queries"),
|
|
215
228
|
tracked: z.boolean().describe("true to track weekly, false to pause"),
|
|
@@ -229,17 +242,20 @@ export async function startMcpServer() {
|
|
|
229
242
|
|
|
230
243
|
server.tool(
|
|
231
244
|
"get_competitors",
|
|
232
|
-
"Tracked competitors used in share-of-voice and visibility comparisons.",
|
|
245
|
+
"Tracked competitors used in share-of-voice and visibility comparisons, with each one's AI visibility score, citations of checks over the last 30 days, domain metrics and positioning analysis.",
|
|
233
246
|
{},
|
|
234
247
|
run(() => api.competitors())
|
|
235
248
|
);
|
|
236
249
|
|
|
237
250
|
server.tool(
|
|
238
251
|
"add_competitor",
|
|
239
|
-
"Track a competitor (max 10). They enter weekly share-of-voice comparisons against the org's own citation rate.",
|
|
252
|
+
"Track a competitor (max 10). They enter weekly share-of-voice comparisons against the org's own citation rate. Omit websiteUrl to have the site resolved from the name.",
|
|
240
253
|
{
|
|
241
254
|
name: z.string().describe("Competitor name"),
|
|
242
|
-
websiteUrl: z
|
|
255
|
+
websiteUrl: z
|
|
256
|
+
.string()
|
|
257
|
+
.optional()
|
|
258
|
+
.describe("Competitor website URL (optional; resolved from the name when omitted)"),
|
|
243
259
|
},
|
|
244
260
|
run(({ name, websiteUrl }) => api.addCompetitor(name, websiteUrl))
|
|
245
261
|
);
|
|
@@ -260,7 +276,7 @@ export async function startMcpServer() {
|
|
|
260
276
|
|
|
261
277
|
server.tool(
|
|
262
278
|
"set_topics",
|
|
263
|
-
"Replace the FULL pillar list. Send every topic that should exist; missing ones are safe-deleted, new ones created, duplicates merged. Read get_topics first and modify that list.",
|
|
279
|
+
"Replace the FULL pillar list. Send every topic that should exist; missing ones are safe-deleted, new ones created, duplicates merged. Read get_topics first and modify that list. Up to 20 topics.",
|
|
264
280
|
{
|
|
265
281
|
topics: z.array(z.string()).describe("The complete list of topic names"),
|
|
266
282
|
},
|
|
@@ -269,7 +285,7 @@ export async function startMcpServer() {
|
|
|
269
285
|
|
|
270
286
|
server.tool(
|
|
271
287
|
"delete_planned_title",
|
|
272
|
-
"Remove a planned title from the content calendar. The title returns to Content Ideas as a
|
|
288
|
+
"Remove a planned title from the content calendar. The title's keyword returns to Content Ideas as a saved query, so the topic is not lost. Only planned rows qualify; use archive for generated articles.",
|
|
273
289
|
{ contentId: z.string().describe("Planned contentPage id") },
|
|
274
290
|
run(({ contentId }) => api.deletePlanned(contentId))
|
|
275
291
|
);
|
|
@@ -281,6 +297,18 @@ export async function startMcpServer() {
|
|
|
281
297
|
run(({ days }) => api.trafficOverview(days ? Number(days) : 30))
|
|
282
298
|
);
|
|
283
299
|
|
|
300
|
+
server.tool(
|
|
301
|
+
"get_rankings",
|
|
302
|
+
"Verified Google and Bing positions per keyword (top 30), with Search Console clicks, impressions and average position when connected (gscPosition is an average over the searches you appeared in, never a rank), which page earned each query and whether it is one of your articles (isOurs), and how often Google's AI Overview shows above each keyword and cites one of your pages (a rate over the last 8 weekly checks). Defaults to queries your published articles appear for.",
|
|
303
|
+
{
|
|
304
|
+
scope: z
|
|
305
|
+
.enum(["ours", "site"])
|
|
306
|
+
.optional()
|
|
307
|
+
.describe('"ours" (default): queries your published articles appear for. "site": everything on the domain.'),
|
|
308
|
+
},
|
|
309
|
+
run(({ scope }) => api.rankings(scope ?? "ours"))
|
|
310
|
+
);
|
|
311
|
+
|
|
284
312
|
server.tool(
|
|
285
313
|
"get_planning_capacity",
|
|
286
314
|
"How many article titles can still be planned onto the content calendar within the scheduling horizon. Check before plan_content.",
|
|
@@ -360,7 +388,7 @@ export async function startMcpServer() {
|
|
|
360
388
|
|
|
361
389
|
server.tool(
|
|
362
390
|
"queue_outreach_email",
|
|
363
|
-
"Queue an outreach email to a prospect. It sends from the workspace's own connected mailbox, paced automatically with a daily cap. Uses the stored draft unless subject/body are given. Dry-run unless confirm is true — a confirmed queue leads to a REAL email being sent.",
|
|
391
|
+
"Queue an outreach email to a prospect. It sends from the workspace's own connected mailbox, paced automatically with a daily cap. Uses the stored draft unless subject/body are given; a \"[Your name]\" sign-off in the draft is replaced with the owner's profile name at send time. Dry-run unless confirm is true — a confirmed queue leads to a REAL email being sent.",
|
|
364
392
|
{
|
|
365
393
|
backlinkId: z.string().describe("Prospect id from list_outreach_prospects"),
|
|
366
394
|
subject: z.string().optional().describe("Override the drafted subject"),
|
|
@@ -400,7 +428,7 @@ export async function startMcpServer() {
|
|
|
400
428
|
|
|
401
429
|
server.tool(
|
|
402
430
|
"commit_planned_titles",
|
|
403
|
-
"Approve reviewed titles onto the content calendar (the approval half of the plan→commit gate). Pass rejected candidates in `discarded` so their keywords recycle into
|
|
431
|
+
"Approve reviewed titles onto the content calendar (the approval half of the plan→commit gate). Pass rejected candidates in `discarded` so their keywords recycle into Content Ideas.",
|
|
404
432
|
{
|
|
405
433
|
titles: z.array(plannedTitle).describe("Titles to schedule"),
|
|
406
434
|
discarded: z.array(plannedTitle).optional().describe("Rejected candidates"),
|
|
@@ -410,7 +438,7 @@ export async function startMcpServer() {
|
|
|
410
438
|
|
|
411
439
|
server.tool(
|
|
412
440
|
"publish_content",
|
|
413
|
-
"Publish an article to the customer's connected CMS. Defaults to a DRY RUN describing what would happen; a human should approve before calling again with confirm=true.",
|
|
441
|
+
"Publish an article to the customer's connected CMS. Defaults to a DRY RUN describing what would happen; a human should approve before calling again with confirm=true. Delivery runs after this call returns. If the article does not go live, list_content shows its lastPublishError. Webhook receivers: https://rctrl.com/docs/webhook",
|
|
414
442
|
{
|
|
415
443
|
contentId: z.string().describe("The content page id"),
|
|
416
444
|
confirm: z.boolean().optional().describe("Set true to actually publish (default: dry run)"),
|
|
@@ -446,7 +474,7 @@ export async function startMcpServer() {
|
|
|
446
474
|
|
|
447
475
|
const articleSettingsShape = {
|
|
448
476
|
autoPublish: z.boolean().optional(),
|
|
449
|
-
autoGenerate: z.boolean().optional().describe("Off = fully manual mode: articles are written only on demand (generate_article) and publish only from the editor"),
|
|
477
|
+
autoGenerate: z.boolean().optional().describe("Off = fully manual mode: articles are written only on demand (generate_article) and publish only from the editor. Turning it on needs a publishing destination that has already put at least one article live on the site."),
|
|
450
478
|
includeInfographics: z.boolean().optional().describe("AI hero + section images"),
|
|
451
479
|
titleInHeroImage: z.boolean().optional().describe("Write the post title into the hero image"),
|
|
452
480
|
includeSectionInfographics: z.boolean().optional().describe("Informational panels in 2,000+ word articles"),
|
|
@@ -481,12 +509,12 @@ export async function startMcpServer() {
|
|
|
481
509
|
|
|
482
510
|
server.tool(
|
|
483
511
|
"reschedule_article",
|
|
484
|
-
"Move a PLANNED article to another day (requires the flexible schedule; max 5/day; the exact hour is placed automatically). Pass the target day as
|
|
512
|
+
"Move a PLANNED article to another day (requires the flexible schedule; max 5/day; the exact hour is placed automatically). Pass the target day as YYYY-MM-DD in the workspace's timezone.",
|
|
485
513
|
{
|
|
486
514
|
contentId: z.string(),
|
|
487
|
-
|
|
515
|
+
date: z.string().describe("Target day as YYYY-MM-DD in the workspace's timezone"),
|
|
488
516
|
},
|
|
489
|
-
run(({ contentId,
|
|
517
|
+
run(({ contentId, date }) => api.reschedule(contentId, date))
|
|
490
518
|
);
|
|
491
519
|
|
|
492
520
|
server.tool(
|
|
@@ -522,7 +550,7 @@ export async function startMcpServer() {
|
|
|
522
550
|
|
|
523
551
|
server.tool(
|
|
524
552
|
"list_repurpose_queue",
|
|
525
|
-
"Published articles with their social repurpose drafts per platform (platform + status chips). Shows which articles still need drafts and
|
|
553
|
+
"Published articles with their social repurpose drafts per platform (platform + status chips). Shows which articles still need drafts and which scheduler is connected (Buffer or Postiz).",
|
|
526
554
|
{},
|
|
527
555
|
run(() => api.repurposeQueue())
|
|
528
556
|
);
|
|
@@ -536,7 +564,7 @@ export async function startMcpServer() {
|
|
|
536
564
|
|
|
537
565
|
server.tool(
|
|
538
566
|
"generate_repurpose_drafts",
|
|
539
|
-
"Draft platform-native social posts for a published article.
|
|
567
|
+
"Draft platform-native social posts for a published article. Only for an article with no drafts yet; edit or dismiss existing drafts in the dashboard. Defaults to a DRY RUN; confirm=true generates (costly).",
|
|
540
568
|
{
|
|
541
569
|
contentId: z.string(),
|
|
542
570
|
platforms: z
|
|
@@ -572,33 +600,37 @@ export async function startMcpServer() {
|
|
|
572
600
|
|
|
573
601
|
server.tool(
|
|
574
602
|
"mark_repurpose_posted",
|
|
575
|
-
"Mark a repurpose draft as posted — use after publishing its text manually (outside Postiz).",
|
|
603
|
+
"Mark a repurpose draft as posted — use after publishing its text manually (outside Buffer or Postiz).",
|
|
576
604
|
{ draftId: z.string() },
|
|
577
605
|
run(({ draftId }) => api.repurposeMarkPosted(draftId))
|
|
578
606
|
);
|
|
579
607
|
|
|
580
608
|
server.tool(
|
|
581
609
|
"list_postiz_channels",
|
|
582
|
-
"Connected
|
|
610
|
+
"Connected social channels across Postiz and Buffer (scheduler, id, name, platform). Channel ids are required by push_repurpose_draft.",
|
|
583
611
|
{},
|
|
584
612
|
run(() => api.repurposeChannels())
|
|
585
613
|
);
|
|
586
614
|
|
|
587
615
|
server.tool(
|
|
588
616
|
"push_repurpose_draft",
|
|
589
|
-
"Send a repurpose draft to connected
|
|
617
|
+
"Send a repurpose draft to a connected scheduler's channels: Postiz or Buffer (post now, schedule, save as a draft, or add to Buffer's queue). PUBLICLY VISIBLE side effect: defaults to a DRY RUN; a human should approve before calling again with confirm=true.",
|
|
590
618
|
{
|
|
591
619
|
draftId: z.string(),
|
|
620
|
+
scheduler: z
|
|
621
|
+
.enum(["postiz", "buffer"])
|
|
622
|
+
.optional()
|
|
623
|
+
.describe("Which scheduler; required when both are connected"),
|
|
592
624
|
integrationIds: z
|
|
593
625
|
.array(z.string())
|
|
594
626
|
.optional()
|
|
595
627
|
.describe(
|
|
596
|
-
"
|
|
628
|
+
"Channel ids from list_postiz_channels (required with confirm)"
|
|
597
629
|
),
|
|
598
630
|
scheduleType: z
|
|
599
|
-
.enum(["now", "schedule", "draft"])
|
|
631
|
+
.enum(["now", "schedule", "draft", "queue"])
|
|
600
632
|
.optional()
|
|
601
|
-
.describe("Default now"),
|
|
633
|
+
.describe("Default now; queue is Buffer only"),
|
|
602
634
|
date: z.string().optional().describe("ISO time for scheduleType=schedule"),
|
|
603
635
|
confirm: z.boolean().optional(),
|
|
604
636
|
},
|
|
@@ -662,13 +694,13 @@ export async function startMcpServer() {
|
|
|
662
694
|
|
|
663
695
|
server.tool(
|
|
664
696
|
"list_backlinks",
|
|
665
|
-
"The workspace's backlink table, newest first.",
|
|
697
|
+
"The workspace's backlink table, newest first. Each row carries its source (index, outreach, managed, network).",
|
|
666
698
|
{
|
|
667
699
|
status: z
|
|
668
700
|
.string()
|
|
669
701
|
.optional()
|
|
670
702
|
.describe(
|
|
671
|
-
"Filter: discovered, verified, lost, identified, contacted, replied, link_placed, rejected"
|
|
703
|
+
"Filter: discovered, verified, lost, placed, identified, contacted, replied, link_placed, rejected"
|
|
672
704
|
),
|
|
673
705
|
},
|
|
674
706
|
run(({ status }) => api.backlinks(status))
|
|
@@ -732,11 +764,25 @@ export async function startMcpServer() {
|
|
|
732
764
|
|
|
733
765
|
server.tool(
|
|
734
766
|
"update_social_thread_status",
|
|
735
|
-
"
|
|
767
|
+
"Skip a social thread so it leaves the queue (the only manual status; Posted is set automatically once your reply is found in the thread).",
|
|
736
768
|
{ threadId: z.string(), status: z.string() },
|
|
737
769
|
run(({ threadId, status }) => api.socialStatus(threadId, status))
|
|
738
770
|
);
|
|
739
771
|
|
|
772
|
+
server.tool(
|
|
773
|
+
"open_social_thread",
|
|
774
|
+
"Open a drafted social thread to post the reply yourself: returns the thread URL (for X, a link that opens X's composer with the reply filled in) and starts four checks over 72 hours that mark the thread Posted once your reply is found there. Pass text to use an edited reply; X replies must be 280 characters or fewer.",
|
|
775
|
+
{ threadId: z.string(), text: z.string().optional() },
|
|
776
|
+
run(({ threadId, text }) => api.socialOpen(threadId, text))
|
|
777
|
+
);
|
|
778
|
+
|
|
779
|
+
server.tool(
|
|
780
|
+
"set_social_x_handle",
|
|
781
|
+
"Save the X handle used to recognise your posted replies (optional; text matching covers the rest). Pass an empty handle to clear it.",
|
|
782
|
+
{ handle: z.string() },
|
|
783
|
+
run(({ handle }) => api.socialXHandle(handle))
|
|
784
|
+
);
|
|
785
|
+
|
|
740
786
|
server.tool(
|
|
741
787
|
"draft_social_reply",
|
|
742
788
|
"AI-draft a reply for a social thread. Nothing posts — the draft lands on the thread for human review. Subject to the same warm-up/pacing guards as the dashboard.",
|
|
@@ -849,6 +895,30 @@ export async function startMcpServer() {
|
|
|
849
895
|
run((args) => api.brandIcpWrite(args))
|
|
850
896
|
);
|
|
851
897
|
|
|
898
|
+
server.tool(
|
|
899
|
+
"suggest_brand_icps",
|
|
900
|
+
"Suggest up to 8 audiences for this site from its brand, products, competitors and topics. Saves nothing. Pick one and pass it to draft_brand_icp, or write your own with write_brand_icp.",
|
|
901
|
+
{ exclude: z.array(z.string()).optional().describe("Audience titles already shown, to get different ones") },
|
|
902
|
+
run(({ exclude }) => api.suggestIcps(exclude ?? []))
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
server.tool(
|
|
906
|
+
"draft_brand_icp",
|
|
907
|
+
"Draft one buyer profile from a one-sentence description or a suggestion from suggest_brand_icps. Returns the fields to review; nothing is saved until write_brand_icp. Fields passed in given are kept as written.",
|
|
908
|
+
{
|
|
909
|
+
describe: z.string().optional().describe("Who the audience is, in one sentence"),
|
|
910
|
+
title: z.string().optional().describe("Title of a suggestion from suggest_brand_icps"),
|
|
911
|
+
who: z.string().optional().describe("The suggestion's one-line description"),
|
|
912
|
+
given: z
|
|
913
|
+
.record(z.string(), z.unknown())
|
|
914
|
+
.optional()
|
|
915
|
+
.describe("Fields to keep as written (same keys as write_brand_icp, plus ageRange, seniority, department as strings). A key you pass is kept, empty included; a key you leave out is filled."),
|
|
916
|
+
},
|
|
917
|
+
run(({ describe, title, who, given }) =>
|
|
918
|
+
api.draftIcp({ describe, suggestion: title ? { title, who } : undefined, given })
|
|
919
|
+
)
|
|
920
|
+
);
|
|
921
|
+
|
|
852
922
|
await server.connect(new StdioServerTransport());
|
|
853
923
|
// Keep the process alive; the transport owns stdin/stdout from here
|
|
854
924
|
console.error("rankcontrol MCP server running on stdio");
|