rankcontrol 0.9.0 → 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 +19 -9
- package/package.json +1 -1
- package/src/cli.mjs +62 -8
- package/src/client.mjs +13 -1
- package/src/mcp.mjs +80 -17
package/README.md
CHANGED
|
@@ -39,6 +39,7 @@ 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>
|
|
@@ -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>
|
|
@@ -101,8 +104,10 @@ npx rankcontrol repurpose-push <draftId> --channels <id> --confirm #
|
|
|
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
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,7 +241,7 @@ 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
|
|
@@ -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
|
|
|
@@ -535,7 +548,7 @@ export function runCli(argv) {
|
|
|
535
548
|
|
|
536
549
|
program
|
|
537
550
|
.command("repurpose-generate <contentId>")
|
|
538
|
-
.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)")
|
|
539
552
|
.option(
|
|
540
553
|
"--platforms <a,b>",
|
|
541
554
|
"Subset: linkedin,twitter,pinterest,instagram,facebook,threads,youtube,tiktok"
|
|
@@ -708,11 +721,28 @@ export function runCli(argv) {
|
|
|
708
721
|
|
|
709
722
|
program
|
|
710
723
|
.command("social-status <threadId> <status>")
|
|
711
|
-
.description("
|
|
724
|
+
.description("Skip a thread (the only manual status; Posted is detected automatically)")
|
|
712
725
|
.action((threadId, status) =>
|
|
713
726
|
api.socialStatus(threadId, status).then(out).catch(fail)
|
|
714
727
|
);
|
|
715
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
|
+
|
|
716
746
|
program
|
|
717
747
|
.command("social-draft-reply <threadId>")
|
|
718
748
|
.description("AI-draft a reply for a thread (nothing posts; the draft lands for review)")
|
|
@@ -774,5 +804,29 @@ export function runCli(argv) {
|
|
|
774
804
|
api.brandIcpWrite(parseJson(opts.json, "brand-icp")).then(out).catch(fail)
|
|
775
805
|
);
|
|
776
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
|
+
|
|
777
831
|
program.parseAsync(argv);
|
|
778
832
|
}
|
package/src/client.mjs
CHANGED
|
@@ -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/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,7 +242,7 @@ 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
|
);
|
|
@@ -263,7 +276,7 @@ export async function startMcpServer() {
|
|
|
263
276
|
|
|
264
277
|
server.tool(
|
|
265
278
|
"set_topics",
|
|
266
|
-
"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.",
|
|
267
280
|
{
|
|
268
281
|
topics: z.array(z.string()).describe("The complete list of topic names"),
|
|
269
282
|
},
|
|
@@ -272,7 +285,7 @@ export async function startMcpServer() {
|
|
|
272
285
|
|
|
273
286
|
server.tool(
|
|
274
287
|
"delete_planned_title",
|
|
275
|
-
"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.",
|
|
276
289
|
{ contentId: z.string().describe("Planned contentPage id") },
|
|
277
290
|
run(({ contentId }) => api.deletePlanned(contentId))
|
|
278
291
|
);
|
|
@@ -284,6 +297,18 @@ export async function startMcpServer() {
|
|
|
284
297
|
run(({ days }) => api.trafficOverview(days ? Number(days) : 30))
|
|
285
298
|
);
|
|
286
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
|
+
|
|
287
312
|
server.tool(
|
|
288
313
|
"get_planning_capacity",
|
|
289
314
|
"How many article titles can still be planned onto the content calendar within the scheduling horizon. Check before plan_content.",
|
|
@@ -363,7 +388,7 @@ export async function startMcpServer() {
|
|
|
363
388
|
|
|
364
389
|
server.tool(
|
|
365
390
|
"queue_outreach_email",
|
|
366
|
-
"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.",
|
|
367
392
|
{
|
|
368
393
|
backlinkId: z.string().describe("Prospect id from list_outreach_prospects"),
|
|
369
394
|
subject: z.string().optional().describe("Override the drafted subject"),
|
|
@@ -403,7 +428,7 @@ export async function startMcpServer() {
|
|
|
403
428
|
|
|
404
429
|
server.tool(
|
|
405
430
|
"commit_planned_titles",
|
|
406
|
-
"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.",
|
|
407
432
|
{
|
|
408
433
|
titles: z.array(plannedTitle).describe("Titles to schedule"),
|
|
409
434
|
discarded: z.array(plannedTitle).optional().describe("Rejected candidates"),
|
|
@@ -413,7 +438,7 @@ export async function startMcpServer() {
|
|
|
413
438
|
|
|
414
439
|
server.tool(
|
|
415
440
|
"publish_content",
|
|
416
|
-
"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",
|
|
417
442
|
{
|
|
418
443
|
contentId: z.string().describe("The content page id"),
|
|
419
444
|
confirm: z.boolean().optional().describe("Set true to actually publish (default: dry run)"),
|
|
@@ -449,7 +474,7 @@ export async function startMcpServer() {
|
|
|
449
474
|
|
|
450
475
|
const articleSettingsShape = {
|
|
451
476
|
autoPublish: z.boolean().optional(),
|
|
452
|
-
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
|
|
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."),
|
|
453
478
|
includeInfographics: z.boolean().optional().describe("AI hero + section images"),
|
|
454
479
|
titleInHeroImage: z.boolean().optional().describe("Write the post title into the hero image"),
|
|
455
480
|
includeSectionInfographics: z.boolean().optional().describe("Informational panels in 2,000+ word articles"),
|
|
@@ -525,7 +550,7 @@ export async function startMcpServer() {
|
|
|
525
550
|
|
|
526
551
|
server.tool(
|
|
527
552
|
"list_repurpose_queue",
|
|
528
|
-
"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).",
|
|
529
554
|
{},
|
|
530
555
|
run(() => api.repurposeQueue())
|
|
531
556
|
);
|
|
@@ -539,7 +564,7 @@ export async function startMcpServer() {
|
|
|
539
564
|
|
|
540
565
|
server.tool(
|
|
541
566
|
"generate_repurpose_drafts",
|
|
542
|
-
"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).",
|
|
543
568
|
{
|
|
544
569
|
contentId: z.string(),
|
|
545
570
|
platforms: z
|
|
@@ -575,7 +600,7 @@ export async function startMcpServer() {
|
|
|
575
600
|
|
|
576
601
|
server.tool(
|
|
577
602
|
"mark_repurpose_posted",
|
|
578
|
-
"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).",
|
|
579
604
|
{ draftId: z.string() },
|
|
580
605
|
run(({ draftId }) => api.repurposeMarkPosted(draftId))
|
|
581
606
|
);
|
|
@@ -739,11 +764,25 @@ export async function startMcpServer() {
|
|
|
739
764
|
|
|
740
765
|
server.tool(
|
|
741
766
|
"update_social_thread_status",
|
|
742
|
-
"
|
|
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).",
|
|
743
768
|
{ threadId: z.string(), status: z.string() },
|
|
744
769
|
run(({ threadId, status }) => api.socialStatus(threadId, status))
|
|
745
770
|
);
|
|
746
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
|
+
|
|
747
786
|
server.tool(
|
|
748
787
|
"draft_social_reply",
|
|
749
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.",
|
|
@@ -856,6 +895,30 @@ export async function startMcpServer() {
|
|
|
856
895
|
run((args) => api.brandIcpWrite(args))
|
|
857
896
|
);
|
|
858
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
|
+
|
|
859
922
|
await server.connect(new StdioServerTransport());
|
|
860
923
|
// Keep the process alive; the transport owns stdin/stdout from here
|
|
861
924
|
console.error("rankcontrol MCP server running on stdio");
|