rankcontrol 0.2.0 → 0.6.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 +47 -2
- package/package.json +1 -1
- package/src/cli.mjs +486 -1
- package/src/client.mjs +102 -0
- package/src/mcp.mjs +545 -2
package/README.md
CHANGED
|
@@ -29,13 +29,38 @@ The env var wins over the stored config when both exist. Scopes are set per key;
|
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
npx rankcontrol funnel # AI crawls / AI visits / AI leads (30d)
|
|
32
|
+
npx rankcontrol score # composite visibility score + subscores (AI/Google/Bing)
|
|
33
|
+
npx rankcontrol leads --limit 100 # captured leads with AI source attribution
|
|
34
|
+
npx rankcontrol traffic --days 30 # page views, visitors, bounce rate, time on page
|
|
32
35
|
npx rankcontrol visibility --days 60 # daily AI visibility score trend
|
|
36
|
+
npx rankcontrol sov --days 30 # share of voice vs competitors
|
|
37
|
+
npx rankcontrol sources --days 30 # cited domains typed brand/competitive/ugc/editorial
|
|
38
|
+
npx rankcontrol sentiment --days 30 # how AI answers frame your brand
|
|
39
|
+
npx rankcontrol optimizer --limit 25 # pages ranked by citability, worst first
|
|
33
40
|
npx rankcontrol citations --model chatgpt
|
|
41
|
+
npx rankcontrol queries # tracked queries checked weekly across AI engines
|
|
34
42
|
npx rankcontrol capacity # remaining content-plan slots
|
|
35
43
|
npx rankcontrol plan-content --topics "bus travel" --max-difficulty 40
|
|
36
44
|
npx rankcontrol commit-titles --file titles.json
|
|
37
45
|
npx rankcontrol publish <contentId> # dry run
|
|
38
46
|
npx rankcontrol publish <contentId> --confirm # actually publish
|
|
47
|
+
npx rankcontrol generate <contentId> # dry run
|
|
48
|
+
npx rankcontrol generate <contentId> --confirm # write a planned article now (slot unchanged)
|
|
49
|
+
npx rankcontrol settings # article policy (auto-write, auto-publish, images, ...)
|
|
50
|
+
npx rankcontrol settings-set --auto-generate off --auto-publish off
|
|
51
|
+
npx rankcontrol reschedule <contentId> 2026-08-04
|
|
52
|
+
npx rankcontrol internal-links <contentId> # who links here / links out
|
|
53
|
+
npx rankcontrol site-pages # pages used for internal links
|
|
54
|
+
npx rankcontrol detect-links https://example.com/sitemap.xml
|
|
55
|
+
npx rankcontrol add-pages https://example.com/pricing
|
|
56
|
+
npx rankcontrol repurpose # published articles + social draft chips
|
|
57
|
+
npx rankcontrol repurpose <contentId> # full drafts for one article
|
|
58
|
+
npx rankcontrol repurpose-generate <contentId> --platforms twitter --confirm
|
|
59
|
+
npx rankcontrol repurpose-edit <draftId> --title "New hook"
|
|
60
|
+
npx rankcontrol repurpose-channels # Postiz channel ids (for repurpose-push)
|
|
61
|
+
npx rankcontrol repurpose-push <draftId> --channels <id> # dry run
|
|
62
|
+
npx rankcontrol repurpose-push <draftId> --channels <id> --confirm
|
|
63
|
+
npx rankcontrol repurpose-mark-posted <draftId> # posted it yourself
|
|
39
64
|
npx rankcontrol jobs # async job status (agent runs)
|
|
40
65
|
```
|
|
41
66
|
|
|
@@ -66,14 +91,34 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
66
91
|
| Tool | What it does |
|
|
67
92
|
|---|---|
|
|
68
93
|
| `get_overview_funnel` | AI crawls / AI visits / AI leads, last 30 days |
|
|
94
|
+
| `list_leads` | Captured leads with AI model / query / page attribution |
|
|
95
|
+
| `get_traffic_overview` | Page views, visitors, sessions, bounce rate, time on page |
|
|
69
96
|
| `get_visibility_trend` | Daily visibility score from stored weekly citation checks |
|
|
70
97
|
| `list_citations` | Recent citation checks per AI model |
|
|
71
|
-
| `
|
|
98
|
+
| `list_tracked_queries` | The queries checked weekly across the AI engines |
|
|
99
|
+
| `get_visibility_score` | Composite score (50% AI + 30% Google + 20% Bing) with subscores |
|
|
100
|
+
| `get_share_of_voice` | Your citation rate vs competitor mention rates + your share |
|
|
101
|
+
| `get_citation_sources` | Cited domains typed brand / competitive / UGC / editorial |
|
|
102
|
+
| `get_citation_sentiment` | Positive / neutral / negative framing with recent receipts |
|
|
103
|
+
| `get_citability_report` | Published pages by citability score, worst first, with fixes |
|
|
72
104
|
| `get_planning_capacity` | Remaining calendar slots (check before planning) |
|
|
73
105
|
| `plan_content` | Generate candidate titles (capacity-gated, nothing scheduled) |
|
|
74
106
|
| `commit_planned_titles` | Approve reviewed titles onto the calendar |
|
|
75
107
|
| `publish_content` | Dry run by default; `confirm: true` publishes |
|
|
108
|
+
| `generate_article` | Write a planned article's body now, slot unchanged; dry run by default |
|
|
109
|
+
| `get_article_settings` / `update_article_settings` | Read / merge-patch the article policy (auto-write, auto-publish, images, links, ...) |
|
|
110
|
+
| `reschedule_article` | Move a planned article to another day (flexible schedule) |
|
|
111
|
+
| `get_internal_links` | Who links to this article internally, and its outbound count |
|
|
112
|
+
| `list_site_pages` / `add_site_pages` | Pages used for in-article internal links |
|
|
113
|
+
| `detect_site_links` | Scan a sitemap or blog root for site pages |
|
|
114
|
+
| `list_repurpose_queue` | Published articles + social draft chips per platform |
|
|
115
|
+
| `get_repurpose_drafts` | Full social drafts for one article (8 platforms) |
|
|
116
|
+
| `generate_repurpose_drafts` | Draft social posts for an article; dry run by default |
|
|
117
|
+
| `update_repurpose_draft` | Edit a draft's body/title before pushing |
|
|
118
|
+
| `mark_repurpose_posted` | Record a draft as posted manually |
|
|
119
|
+
| `list_postiz_channels` | Connected Postiz channels (ids for pushing) |
|
|
120
|
+
| `push_repurpose_draft` | Send a draft to Postiz; dry run by default |
|
|
76
121
|
| `list_content` | Content pages with status |
|
|
77
122
|
| `list_jobs` | Async job status (agent runs) |
|
|
78
123
|
|
|
79
|
-
The plan→commit split is intentional: `plan_content` proposes, a human (or a supervising agent) reviews, `commit_planned_titles` approves. `publish_content`
|
|
124
|
+
The plan→commit split is intentional: `plan_content` proposes, a human (or a supervising agent) reviews, `commit_planned_titles` approves. `publish_content` and `generate_article` are dry-run-first for the same reason.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -12,13 +12,16 @@ const fail = (err) => {
|
|
|
12
12
|
const list = (val) => val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13
13
|
|
|
14
14
|
export function runCli(argv) {
|
|
15
|
+
const pkg = JSON.parse(
|
|
16
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8")
|
|
17
|
+
);
|
|
15
18
|
const program = new Command();
|
|
16
19
|
program
|
|
17
20
|
.name("rankcontrol")
|
|
18
21
|
.description(
|
|
19
22
|
"RankControl from the terminal. Auth: `rankcontrol login` (browser approval) or export RANKCONTROL_API_KEY=rctrl_pk_..."
|
|
20
23
|
)
|
|
21
|
-
.version(
|
|
24
|
+
.version(pkg.version);
|
|
22
25
|
|
|
23
26
|
program
|
|
24
27
|
.command("mcp")
|
|
@@ -50,12 +53,53 @@ export function runCli(argv) {
|
|
|
50
53
|
.description("AI pipeline last 30 days: crawls, AI visits, AI leads")
|
|
51
54
|
.action(() => api.overviewFunnel().then(out).catch(fail));
|
|
52
55
|
|
|
56
|
+
program
|
|
57
|
+
.command("leads")
|
|
58
|
+
.description("Captured leads with source attribution (AI model, query, page)")
|
|
59
|
+
.option("--limit <n>", "Max rows, up to 500", "100")
|
|
60
|
+
.action((opts) => api.leads(Number(opts.limit)).then(out).catch(fail));
|
|
61
|
+
|
|
62
|
+
program
|
|
63
|
+
.command("traffic")
|
|
64
|
+
.description("Traffic overview: page views, visitors, sessions, bounce rate, time on page")
|
|
65
|
+
.option("--days <n>", "7, 30 or 90", "30")
|
|
66
|
+
.action((opts) => api.trafficOverview(Number(opts.days)).then(out).catch(fail));
|
|
67
|
+
|
|
68
|
+
program
|
|
69
|
+
.command("score")
|
|
70
|
+
.description("Composite visibility score: 50% AI citation rate + 30% Google + 20% Bing rank share, with subscores")
|
|
71
|
+
.action(() => api.visibilityScore().then(out).catch(fail));
|
|
72
|
+
|
|
53
73
|
program
|
|
54
74
|
.command("visibility")
|
|
55
75
|
.description("Daily AI visibility score trend from the stored weekly citation checks")
|
|
56
76
|
.option("--days <n>", "30, 60 or 90", "30")
|
|
57
77
|
.action((opts) => api.visibilityTrend(Number(opts.days)).then(out).catch(fail));
|
|
58
78
|
|
|
79
|
+
program
|
|
80
|
+
.command("sov")
|
|
81
|
+
.description("Share of voice: your citation rate vs top competitors, plus your share of all brand appearances")
|
|
82
|
+
.option("--days <n>", "Window in days, 7-90", "30")
|
|
83
|
+
.action((opts) => api.shareOfVoice(Number(opts.days)).then(out).catch(fail));
|
|
84
|
+
|
|
85
|
+
program
|
|
86
|
+
.command("sources")
|
|
87
|
+
.description("Domains AI answers cite for your tracked queries, typed brand/competitive/ugc/editorial")
|
|
88
|
+
.option("--days <n>", "Window in days, 7-90", "30")
|
|
89
|
+
.action((opts) => api.citationSources(Number(opts.days)).then(out).catch(fail));
|
|
90
|
+
|
|
91
|
+
program
|
|
92
|
+
.command("sentiment")
|
|
93
|
+
.description("How AI answers frame your brand when cited: positive/neutral/negative with recent receipts")
|
|
94
|
+
.option("--days <n>", "Window in days, 7-90", "30")
|
|
95
|
+
.action((opts) => api.citationSentiment(Number(opts.days)).then(out).catch(fail));
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command("optimizer")
|
|
99
|
+
.description("Published pages ranked by citability score, worst first, with their open fixes")
|
|
100
|
+
.option("--limit <n>", "Max rows", "25")
|
|
101
|
+
.action((opts) => api.optimizer(Number(opts.limit)).then(out).catch(fail));
|
|
102
|
+
|
|
59
103
|
program
|
|
60
104
|
.command("citations")
|
|
61
105
|
.description("Recent AI citation checks")
|
|
@@ -68,11 +112,115 @@ export function runCli(argv) {
|
|
|
68
112
|
.catch(fail)
|
|
69
113
|
);
|
|
70
114
|
|
|
115
|
+
program
|
|
116
|
+
.command("queries")
|
|
117
|
+
.description("List the tracked queries checked weekly across the AI engines")
|
|
118
|
+
.action(() => api.trackedQueries().then(out).catch(fail));
|
|
119
|
+
|
|
120
|
+
program
|
|
121
|
+
.command("crawler-access")
|
|
122
|
+
.description("Daily AI-crawler reachability probe: is the site's edge or robots.txt blocking GPTBot/ClaudeBot")
|
|
123
|
+
.action(() => api.crawlerAccess().then(out).catch(fail));
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command("query-add")
|
|
127
|
+
.description("Add a query to the tracking pool (tracks immediately when a plan slot is free)")
|
|
128
|
+
.argument("<queryText>", "The search/AI query to track")
|
|
129
|
+
.action((queryText) => api.addQuery(queryText).then(out).catch(fail));
|
|
130
|
+
|
|
71
131
|
program
|
|
72
132
|
.command("capacity")
|
|
73
133
|
.description("Remaining plan slots on the content calendar")
|
|
74
134
|
.action(() => api.planningCapacity().then(out).catch(fail));
|
|
75
135
|
|
|
136
|
+
program
|
|
137
|
+
.command("team")
|
|
138
|
+
.description("Workspace members and pending invites (owner surface)")
|
|
139
|
+
.action(() => api.team().then(out).catch(fail));
|
|
140
|
+
|
|
141
|
+
program
|
|
142
|
+
.command("team-invite")
|
|
143
|
+
.description("Invite a member (dry-run by default; --confirm sends the email)")
|
|
144
|
+
.argument("<email>", "Invitee email")
|
|
145
|
+
.option(
|
|
146
|
+
"--perms <area=level,...>",
|
|
147
|
+
"Per-screen levels, e.g. content=write,analytics=read (unspecified screens default to none)"
|
|
148
|
+
)
|
|
149
|
+
.option("--confirm", "Actually send the invite")
|
|
150
|
+
.action((email, opts) => {
|
|
151
|
+
const screenPermissions = {};
|
|
152
|
+
for (const pair of (opts.perms ?? "").split(",").filter(Boolean)) {
|
|
153
|
+
const [area, level] = pair.split("=");
|
|
154
|
+
screenPermissions[area?.trim()] = level?.trim();
|
|
155
|
+
}
|
|
156
|
+
return api
|
|
157
|
+
.teamInvite({
|
|
158
|
+
email,
|
|
159
|
+
...(Object.keys(screenPermissions).length ? { screenPermissions } : {}),
|
|
160
|
+
confirm: !!opts.confirm,
|
|
161
|
+
})
|
|
162
|
+
.then(out)
|
|
163
|
+
.catch(fail);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
program
|
|
167
|
+
.command("team-revoke")
|
|
168
|
+
.description("Revoke a pending invite")
|
|
169
|
+
.argument("<invitationId>", "Invitation id from `team`")
|
|
170
|
+
.action((invitationId) => api.teamRevoke(invitationId).then(out).catch(fail));
|
|
171
|
+
|
|
172
|
+
program
|
|
173
|
+
.command("team-remove")
|
|
174
|
+
.description("Remove a member (requires --confirm)")
|
|
175
|
+
.argument("<userId>", "Member user id from `team`")
|
|
176
|
+
.option("--confirm", "Actually remove the member")
|
|
177
|
+
.action((userId, opts) =>
|
|
178
|
+
api
|
|
179
|
+
.teamRemove({ userId, confirm: !!opts.confirm })
|
|
180
|
+
.then(out)
|
|
181
|
+
.catch(fail)
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
program
|
|
185
|
+
.command("outreach-prospects")
|
|
186
|
+
.description("Link outreach pipeline with any found contact emails")
|
|
187
|
+
.action(() => api.outreachProspects().then(out).catch(fail));
|
|
188
|
+
|
|
189
|
+
program
|
|
190
|
+
.command("outreach-find-contact")
|
|
191
|
+
.description("Find an outreach email for a prospect (scrapes their site)")
|
|
192
|
+
.argument("<backlinkId>", "Prospect id from `outreach-prospects`")
|
|
193
|
+
.action((backlinkId) =>
|
|
194
|
+
api.outreachFindContact(backlinkId).then(out).catch(fail)
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
program
|
|
198
|
+
.command("outreach-draft-reply")
|
|
199
|
+
.description("AI-draft a reply to a prospect's inbound response (nothing sends)")
|
|
200
|
+
.argument("<backlinkId>", "Prospect id from `outreach-prospects`")
|
|
201
|
+
.action((backlinkId) =>
|
|
202
|
+
api.outreachDraftReply(backlinkId).then(out).catch(fail)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
program
|
|
206
|
+
.command("outreach-queue")
|
|
207
|
+
.description("Queue an outreach email to send from the connected mailbox (dry-run by default)")
|
|
208
|
+
.argument("<backlinkId>", "Prospect id from `outreach-prospects`")
|
|
209
|
+
.option("--subject <subject>", "Override the drafted subject")
|
|
210
|
+
.option("--body <body>", "Override the drafted body")
|
|
211
|
+
.option("--confirm", "Actually queue the send")
|
|
212
|
+
.action((backlinkId, opts) =>
|
|
213
|
+
api
|
|
214
|
+
.outreachQueue({
|
|
215
|
+
backlinkId,
|
|
216
|
+
...(opts.subject ? { subject: opts.subject } : {}),
|
|
217
|
+
...(opts.body ? { body: opts.body } : {}),
|
|
218
|
+
confirm: !!opts.confirm,
|
|
219
|
+
})
|
|
220
|
+
.then(out)
|
|
221
|
+
.catch(fail)
|
|
222
|
+
);
|
|
223
|
+
|
|
76
224
|
program
|
|
77
225
|
.command("jobs")
|
|
78
226
|
.description("Recent agent runs (async job status)")
|
|
@@ -131,5 +279,342 @@ export function runCli(argv) {
|
|
|
131
279
|
api.publishContent(contentId, !!opts.confirm).then(out).catch(fail)
|
|
132
280
|
);
|
|
133
281
|
|
|
282
|
+
program
|
|
283
|
+
.command("generate <contentId>")
|
|
284
|
+
.description("Write a planned article's body now, keeping its publish slot (past-due or ≤3 days ahead; dry run unless --confirm)")
|
|
285
|
+
.option("--confirm", "Actually start generation (spends LLM budget)")
|
|
286
|
+
.action((contentId, opts) =>
|
|
287
|
+
api.generateContent(contentId, !!opts.confirm).then(out).catch(fail)
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
program
|
|
291
|
+
.command("settings")
|
|
292
|
+
.description("Show the article policy: per-article defaults + flexible scheduling")
|
|
293
|
+
.action(() => api.articleSettings().then(out).catch(fail));
|
|
294
|
+
|
|
295
|
+
const onOff = (val) => {
|
|
296
|
+
if (val === "on" || val === "true") return true;
|
|
297
|
+
if (val === "off" || val === "false") return false;
|
|
298
|
+
throw new Error(`Expected on|off, got "${val}"`);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
program
|
|
302
|
+
.command("settings-set")
|
|
303
|
+
.description("Merge-patch the article policy (only flags you pass change)")
|
|
304
|
+
.option("--auto-publish <on|off>")
|
|
305
|
+
.option("--auto-generate <on|off>", "Off = fully manual: write articles only on demand (generate command / Write now)")
|
|
306
|
+
.option("--images <on|off>", "AI hero + section images")
|
|
307
|
+
.option("--title-in-hero <on|off>", "Write the post title into the hero image")
|
|
308
|
+
.option("--section-infographics <on|off>", "Informational panels in 2,000+ word articles")
|
|
309
|
+
.option("--related-reading <on|off>", "Related Reading link block at the end")
|
|
310
|
+
.option("--youtube <on|off>", "Embedded video suggestions")
|
|
311
|
+
.option("--emojis <on|off>")
|
|
312
|
+
.option("--internal-links <n>", "Internal links per article (1-20)")
|
|
313
|
+
.option("--external-links <n>", "Outbound authority links per article (0-15)")
|
|
314
|
+
.option("--instructions <text>", "Global editorial instructions")
|
|
315
|
+
.option("--flexible-schedule <on|off>", "1-5 articles/day + calendar drag-and-drop")
|
|
316
|
+
.option(
|
|
317
|
+
"--image-style <set>",
|
|
318
|
+
"Image style set for all generated images: classic-editorial | print-craft | storybook-painterly | modern-saas | dark-premium | bold-poster"
|
|
319
|
+
)
|
|
320
|
+
.action((opts) => {
|
|
321
|
+
try {
|
|
322
|
+
const s = {};
|
|
323
|
+
if (opts.autoPublish !== undefined) s.autoPublish = onOff(opts.autoPublish);
|
|
324
|
+
if (opts.autoGenerate !== undefined) s.autoGenerate = onOff(opts.autoGenerate);
|
|
325
|
+
if (opts.images !== undefined) s.includeInfographics = onOff(opts.images);
|
|
326
|
+
if (opts.titleInHero !== undefined) s.titleInHeroImage = onOff(opts.titleInHero);
|
|
327
|
+
if (opts.sectionInfographics !== undefined)
|
|
328
|
+
s.includeSectionInfographics = onOff(opts.sectionInfographics);
|
|
329
|
+
if (opts.relatedReading !== undefined)
|
|
330
|
+
s.includeRelatedReading = onOff(opts.relatedReading);
|
|
331
|
+
if (opts.youtube !== undefined) s.includeYouTube = onOff(opts.youtube);
|
|
332
|
+
if (opts.emojis !== undefined) s.useEmojis = onOff(opts.emojis);
|
|
333
|
+
if (opts.internalLinks !== undefined)
|
|
334
|
+
s.internalLinksPerArticle = Number(opts.internalLinks);
|
|
335
|
+
if (opts.externalLinks !== undefined)
|
|
336
|
+
s.externalLinksPerArticle = Number(opts.externalLinks);
|
|
337
|
+
if (opts.instructions !== undefined) s.globalInstructions = opts.instructions;
|
|
338
|
+
const body = {};
|
|
339
|
+
if (Object.keys(s).length > 0) body.articleSettings = s;
|
|
340
|
+
if (opts.flexibleSchedule !== undefined)
|
|
341
|
+
body.flexibleScheduling = onOff(opts.flexibleSchedule);
|
|
342
|
+
if (opts.imageStyle !== undefined) body.styleSet = opts.imageStyle;
|
|
343
|
+
if (Object.keys(body).length === 0)
|
|
344
|
+
throw new Error("Pass at least one flag to change (see --help)");
|
|
345
|
+
return api.updateArticleSettings(body).then(out).catch(fail);
|
|
346
|
+
} catch (err) {
|
|
347
|
+
fail(err);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
program
|
|
352
|
+
.command("reschedule <contentId> <date>")
|
|
353
|
+
.description("Move a planned article to a day (YYYY-MM-DD, local time; needs the flexible schedule)")
|
|
354
|
+
.action((contentId, date) => {
|
|
355
|
+
const day = new Date(`${date}T00:00:00`);
|
|
356
|
+
if (Number.isNaN(day.getTime())) return fail(new Error("Date must be YYYY-MM-DD"));
|
|
357
|
+
return api.reschedule(contentId, day.getTime()).then(out).catch(fail);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
program
|
|
361
|
+
.command("internal-links <contentId>")
|
|
362
|
+
.description("Inbound and outbound internal links recorded for an article (from the publish-time link graph)")
|
|
363
|
+
.action((contentId) => api.internalLinks(contentId).then(out).catch(fail));
|
|
364
|
+
|
|
365
|
+
program
|
|
366
|
+
.command("site-pages")
|
|
367
|
+
.description("List the site pages used for in-article links and Related Reading")
|
|
368
|
+
.action(() => api.sitePages().then(out).catch(fail));
|
|
369
|
+
|
|
370
|
+
program
|
|
371
|
+
.command("detect-links <url>")
|
|
372
|
+
.description("Scan a sitemap (nested indexes too) or a blog root page for site pages")
|
|
373
|
+
.option("--blog-root", "Treat the URL as a page to crawl instead of a sitemap")
|
|
374
|
+
.action((url, opts) =>
|
|
375
|
+
api
|
|
376
|
+
.detectSiteLinks(opts.blogRoot ? "blogroot" : "sitemap", url)
|
|
377
|
+
.then(out)
|
|
378
|
+
.catch(fail)
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
program
|
|
382
|
+
.command("add-pages <urls...>")
|
|
383
|
+
.description("Add site page URLs for internal linking (dedupes; titles auto-fill)")
|
|
384
|
+
.action((urls) => api.addSitePages(urls).then(out).catch(fail));
|
|
385
|
+
|
|
386
|
+
program
|
|
387
|
+
.command("repurpose [contentId]")
|
|
388
|
+
.description("Repurpose queue (published articles + draft chips), or full drafts for one article")
|
|
389
|
+
.action((contentId) =>
|
|
390
|
+
(contentId ? api.repurposeDrafts(contentId) : api.repurposeQueue())
|
|
391
|
+
.then(out)
|
|
392
|
+
.catch(fail)
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
program
|
|
396
|
+
.command("repurpose-generate <contentId>")
|
|
397
|
+
.description("Draft social posts for a published article (dry run unless --confirm; spends LLM budget)")
|
|
398
|
+
.option(
|
|
399
|
+
"--platforms <a,b>",
|
|
400
|
+
"Subset: linkedin,twitter,pinterest,instagram,facebook,threads,youtube,tiktok"
|
|
401
|
+
)
|
|
402
|
+
.option("--confirm", "Actually generate")
|
|
403
|
+
.action((contentId, opts) =>
|
|
404
|
+
api
|
|
405
|
+
.repurposeGenerate({
|
|
406
|
+
contentId,
|
|
407
|
+
platforms: opts.platforms ? list(opts.platforms) : undefined,
|
|
408
|
+
confirm: !!opts.confirm,
|
|
409
|
+
})
|
|
410
|
+
.then(out)
|
|
411
|
+
.catch(fail)
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
program
|
|
415
|
+
.command("repurpose-edit <draftId>")
|
|
416
|
+
.description("Edit a repurpose draft's text/title before pushing")
|
|
417
|
+
.option("--body <text>")
|
|
418
|
+
.option("--title <text>")
|
|
419
|
+
.action((draftId, opts) =>
|
|
420
|
+
api
|
|
421
|
+
.repurposeEditDraft({ draftId, body: opts.body, title: opts.title })
|
|
422
|
+
.then(out)
|
|
423
|
+
.catch(fail)
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
program
|
|
427
|
+
.command("repurpose-mark-posted <draftId>")
|
|
428
|
+
.description("Mark a draft as posted (you published it yourself)")
|
|
429
|
+
.action((draftId) => api.repurposeMarkPosted(draftId).then(out).catch(fail));
|
|
430
|
+
|
|
431
|
+
program
|
|
432
|
+
.command("repurpose-channels")
|
|
433
|
+
.description("Connected Postiz channels (ids needed for repurpose-push)")
|
|
434
|
+
.action(() => api.repurposeChannels().then(out).catch(fail));
|
|
435
|
+
|
|
436
|
+
program
|
|
437
|
+
.command("repurpose-push <draftId>")
|
|
438
|
+
.description("Send a draft to Postiz (dry run unless --confirm)")
|
|
439
|
+
.option("--channels <id1,id2>", "Postiz channel ids (see repurpose-channels)")
|
|
440
|
+
.option("--when <now|schedule|draft>", "Postiz mode", "now")
|
|
441
|
+
.option("--date <iso>", "ISO time for --when schedule")
|
|
442
|
+
.option("--confirm", "Actually push (publicly visible)")
|
|
443
|
+
.action((draftId, opts) =>
|
|
444
|
+
api
|
|
445
|
+
.repurposePush({
|
|
446
|
+
draftId,
|
|
447
|
+
integrationIds: opts.channels ? list(opts.channels) : undefined,
|
|
448
|
+
scheduleType: opts.when,
|
|
449
|
+
date: opts.date,
|
|
450
|
+
confirm: !!opts.confirm,
|
|
451
|
+
})
|
|
452
|
+
.then(out)
|
|
453
|
+
.catch(fail)
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const parseJson = (raw, label) => {
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(raw.startsWith("@") ? readFileSync(raw.slice(1), "utf8") : raw);
|
|
459
|
+
} catch {
|
|
460
|
+
fail(new Error(`--json for ${label} must be valid JSON (or @file.json)`));
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
program
|
|
465
|
+
.command("ideas")
|
|
466
|
+
.description("Scored content-idea backlog: uncovered queries, citation gaps, quick wins")
|
|
467
|
+
.action(() => api.contentIdeas().then(out).catch(fail));
|
|
468
|
+
|
|
469
|
+
program
|
|
470
|
+
.command("plan-idea <queryText>")
|
|
471
|
+
.description("Put an idea on the content calendar (uses its stored title, or generates one)")
|
|
472
|
+
.option("--title <title>", "Exact headline to plan")
|
|
473
|
+
.action((queryText, opts) =>
|
|
474
|
+
api.planIdea({ queryText, title: opts.title }).then(out).catch(fail)
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
program
|
|
478
|
+
.command("archive <contentId>")
|
|
479
|
+
.description("Archive an article (removes it from the working set)")
|
|
480
|
+
.action((contentId) => api.archiveContent(contentId).then(out).catch(fail));
|
|
481
|
+
|
|
482
|
+
program
|
|
483
|
+
.command("engagement")
|
|
484
|
+
.description("Per-page views + citations for the last 30 days, with view sparklines")
|
|
485
|
+
.action(() => api.pageEngagement().then(out).catch(fail));
|
|
486
|
+
|
|
487
|
+
program
|
|
488
|
+
.command("report")
|
|
489
|
+
.description("Executive summary: last 30 days vs the 30 before (citations, views, crawls, publishing)")
|
|
490
|
+
.action(() => api.reportSummary().then(out).catch(fail));
|
|
491
|
+
|
|
492
|
+
program
|
|
493
|
+
.command("wins")
|
|
494
|
+
.description("Biggest wins of the last 30 days: top cited page, cite-rate jump, ranking climb, best backlink")
|
|
495
|
+
.action(() => api.reportWins().then(out).catch(fail));
|
|
496
|
+
|
|
497
|
+
program
|
|
498
|
+
.command("agent-activity")
|
|
499
|
+
.description("Recent runs per agent lane (brand control, radar, forge, deploy, ...)")
|
|
500
|
+
.option("--per-agent <n>", "Runs per lane, up to 50", "15")
|
|
501
|
+
.option("--since-days <n>", "Window in days", "30")
|
|
502
|
+
.action((opts) =>
|
|
503
|
+
api
|
|
504
|
+
.agentActivity({ perAgent: Number(opts.perAgent), sinceDays: Number(opts.sinceDays) })
|
|
505
|
+
.then(out)
|
|
506
|
+
.catch(fail)
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
program
|
|
510
|
+
.command("backlinks")
|
|
511
|
+
.description("The backlink table, newest first")
|
|
512
|
+
.option("--status <status>", "Filter: discovered, verified, lost, identified, contacted, replied, link_placed, rejected")
|
|
513
|
+
.action((opts) => api.backlinks(opts.status).then(out).catch(fail));
|
|
514
|
+
|
|
515
|
+
program
|
|
516
|
+
.command("backlink-stats")
|
|
517
|
+
.description("Backlink totals + the outreach pipeline counts")
|
|
518
|
+
.action(() => api.backlinkStats().then(out).catch(fail));
|
|
519
|
+
|
|
520
|
+
program
|
|
521
|
+
.command("outreach-status <backlinkId> <status>")
|
|
522
|
+
.description("Move an outreach prospect through the pipeline (identified|contacted|replied|link_placed|rejected)")
|
|
523
|
+
.action((backlinkId, status) =>
|
|
524
|
+
api.outreachStatus(backlinkId, status).then(out).catch(fail)
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
program
|
|
528
|
+
.command("network")
|
|
529
|
+
.description("Link Network credit stats, membership state, and hosted/received placements")
|
|
530
|
+
.action(() => api.linkNetwork().then(out).catch(fail));
|
|
531
|
+
|
|
532
|
+
program
|
|
533
|
+
.command("network-opt-in <on|off>")
|
|
534
|
+
.description("Join or leave the Link Network (dry run unless --confirm; leaving retires live links)")
|
|
535
|
+
.option("--confirm", "Actually change membership")
|
|
536
|
+
.action((state, opts) =>
|
|
537
|
+
api.linkNetworkOptIn(state === "on", !!opts.confirm).then(out).catch(fail)
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
program
|
|
541
|
+
.command("network-remove-placement <placementId>")
|
|
542
|
+
.description("Retire one network placement (dry run unless --confirm; visible on the partner site)")
|
|
543
|
+
.option("--confirm", "Actually remove it")
|
|
544
|
+
.action((placementId, opts) =>
|
|
545
|
+
api.linkNetworkRemovePlacement(placementId, !!opts.confirm).then(out).catch(fail)
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
program
|
|
549
|
+
.command("social")
|
|
550
|
+
.description("Social thread prospects (Reddit/X) with rules context and intent sort")
|
|
551
|
+
.option("--platform <reddit|twitter>")
|
|
552
|
+
.option("--status <status>")
|
|
553
|
+
.option("--age <ranked|new>")
|
|
554
|
+
.action((opts) =>
|
|
555
|
+
api
|
|
556
|
+
.socialThreads({ platform: opts.platform, status: opts.status, age: opts.age })
|
|
557
|
+
.then(out)
|
|
558
|
+
.catch(fail)
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
program
|
|
562
|
+
.command("social-stats")
|
|
563
|
+
.description("Social thread pipeline counts")
|
|
564
|
+
.action(() => api.socialStats().then(out).catch(fail));
|
|
565
|
+
|
|
566
|
+
program
|
|
567
|
+
.command("social-status <threadId> <status>")
|
|
568
|
+
.description("Move a social thread through the pipeline")
|
|
569
|
+
.action((threadId, status) =>
|
|
570
|
+
api.socialStatus(threadId, status).then(out).catch(fail)
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
program
|
|
574
|
+
.command("social-draft-reply <threadId>")
|
|
575
|
+
.description("AI-draft a reply for a thread (nothing posts; the draft lands for review)")
|
|
576
|
+
.option("--mention <none|natural|founderOpen>", "Brand-mention mode")
|
|
577
|
+
.action((threadId, opts) =>
|
|
578
|
+
api.socialDraftReply(threadId, opts.mention).then(out).catch(fail)
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
program
|
|
582
|
+
.command("support <subject>")
|
|
583
|
+
.description("Send a message or bug report to RankControl support (emails the team, capped 3/min)")
|
|
584
|
+
.requiredOption("--message <text>", "What happened — include errors, ids, and steps")
|
|
585
|
+
.option("--page <url>", "Related dashboard or API URL")
|
|
586
|
+
.action((subject, opts) =>
|
|
587
|
+
api.support({ subject, message: opts.message, pageUrl: opts.page }).then(out).catch(fail)
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
program
|
|
591
|
+
.command("brand")
|
|
592
|
+
.description("Brand profile, products, and buyer profiles (ICPs) in one read")
|
|
593
|
+
.action(() => api.brand().then(out).catch(fail));
|
|
594
|
+
|
|
595
|
+
program
|
|
596
|
+
.command("brand-profile-set")
|
|
597
|
+
.description("Patch brand voice/style fields. --json '{\"tone\":\"...\"}' or --json @file.json")
|
|
598
|
+
.requiredOption("--json <json>", "Fields: tone, primaryColor, secondaryColor, fontFamily, metaTitle, metaDescription, metaKeywords, defaultLocale, flexibleScheduling")
|
|
599
|
+
.action((opts) =>
|
|
600
|
+
api.brandProfileSet(parseJson(opts.json, "brand-profile-set")).then(out).catch(fail)
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
program
|
|
604
|
+
.command("brand-product")
|
|
605
|
+
.description("Create, update, or delete a product. Create: name+description+category; update: productId + fields; delete: productId + del:true")
|
|
606
|
+
.requiredOption("--json <json>", "Product JSON (or @file.json)")
|
|
607
|
+
.action((opts) =>
|
|
608
|
+
api.brandProductWrite(parseJson(opts.json, "brand-product")).then(out).catch(fail)
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
program
|
|
612
|
+
.command("brand-icp")
|
|
613
|
+
.description("Create, update, or delete a buyer profile. Create: title+industry+demographics; update: profileId + fields; delete: profileId + del:true")
|
|
614
|
+
.requiredOption("--json <json>", "Buyer profile JSON (or @file.json)")
|
|
615
|
+
.action((opts) =>
|
|
616
|
+
api.brandIcpWrite(parseJson(opts.json, "brand-icp")).then(out).catch(fail)
|
|
617
|
+
);
|
|
618
|
+
|
|
134
619
|
program.parseAsync(argv);
|
|
135
620
|
}
|
package/src/client.mjs
CHANGED
|
@@ -87,6 +87,14 @@ export const api = {
|
|
|
87
87
|
overviewFunnel: () => request("GET", "/api/v1/overview/funnel"),
|
|
88
88
|
visibilityTrend: (days = 30) =>
|
|
89
89
|
request("GET", `/api/v1/visibility/trend?days=${days}`),
|
|
90
|
+
shareOfVoice: (days = 30) =>
|
|
91
|
+
request("GET", `/api/v1/visibility/share-of-voice?days=${days}`),
|
|
92
|
+
citationSources: (days = 30) =>
|
|
93
|
+
request("GET", `/api/v1/visibility/sources?days=${days}`),
|
|
94
|
+
citationSentiment: (days = 30) =>
|
|
95
|
+
request("GET", `/api/v1/visibility/sentiment?days=${days}`),
|
|
96
|
+
optimizer: (limit = 25) =>
|
|
97
|
+
request("GET", `/api/v1/content/optimizer?limit=${limit}`),
|
|
90
98
|
citations: (params = {}) => {
|
|
91
99
|
const q = new URLSearchParams();
|
|
92
100
|
if (params.model) q.set("model", params.model);
|
|
@@ -103,4 +111,98 @@ export const api = {
|
|
|
103
111
|
request("POST", "/api/v1/content/plan/commit", opts),
|
|
104
112
|
publishContent: (contentId, confirm = false) =>
|
|
105
113
|
request("POST", "/api/v1/content/publish", { contentId, confirm }),
|
|
114
|
+
generateContent: (contentId, confirm = false) =>
|
|
115
|
+
request("POST", "/api/v1/content/generate", { contentId, confirm }),
|
|
116
|
+
internalLinks: (contentId) =>
|
|
117
|
+
request(
|
|
118
|
+
"GET",
|
|
119
|
+
`/api/v1/content/internal-links?contentId=${encodeURIComponent(contentId)}`
|
|
120
|
+
),
|
|
121
|
+
articleSettings: () => request("GET", "/api/v1/settings/articles"),
|
|
122
|
+
updateArticleSettings: (opts) =>
|
|
123
|
+
request("POST", "/api/v1/settings/articles", opts),
|
|
124
|
+
reschedule: (contentId, targetDayStartMs) =>
|
|
125
|
+
request("POST", "/api/v1/content/reschedule", { contentId, targetDayStartMs }),
|
|
126
|
+
sitePages: () => request("GET", "/api/v1/links/pages"),
|
|
127
|
+
detectSiteLinks: (source, url) =>
|
|
128
|
+
request("POST", "/api/v1/links/detect", { source, url }),
|
|
129
|
+
addSitePages: (urls) => request("POST", "/api/v1/links/pages", { urls }),
|
|
130
|
+
leads: (limit = 100) => request("GET", `/api/v1/leads?limit=${limit}`),
|
|
131
|
+
repurposeQueue: () => request("GET", "/api/v1/repurpose"),
|
|
132
|
+
repurposeDrafts: (contentId) =>
|
|
133
|
+
request(
|
|
134
|
+
"GET",
|
|
135
|
+
`/api/v1/repurpose?contentId=${encodeURIComponent(contentId)}`
|
|
136
|
+
),
|
|
137
|
+
repurposeChannels: () => request("GET", "/api/v1/repurpose/channels"),
|
|
138
|
+
repurposeGenerate: (opts) =>
|
|
139
|
+
request("POST", "/api/v1/repurpose/generate", opts),
|
|
140
|
+
repurposeEditDraft: (opts) => request("POST", "/api/v1/repurpose/draft", opts),
|
|
141
|
+
repurposeMarkPosted: (draftId) =>
|
|
142
|
+
request("POST", "/api/v1/repurpose/mark-posted", { draftId }),
|
|
143
|
+
repurposePush: (opts) => request("POST", "/api/v1/repurpose/push", opts),
|
|
144
|
+
trackedQueries: () => request("GET", "/api/v1/queries"),
|
|
145
|
+
addQuery: (queryText) => request("POST", "/api/v1/queries", { queryText }),
|
|
146
|
+
crawlerAccess: () => request("GET", "/api/v1/crawler-access"),
|
|
147
|
+
trafficOverview: (days = 30) =>
|
|
148
|
+
request("GET", `/api/v1/analytics/traffic?days=${days}`),
|
|
149
|
+
team: () => request("GET", "/api/v1/team"),
|
|
150
|
+
teamInvite: (opts) => request("POST", "/api/v1/team/invite", opts),
|
|
151
|
+
teamRevoke: (invitationId) =>
|
|
152
|
+
request("POST", "/api/v1/team/revoke", { invitationId }),
|
|
153
|
+
teamRemove: (opts) => request("POST", "/api/v1/team/remove", opts),
|
|
154
|
+
outreachProspects: () => request("GET", "/api/v1/outreach/prospects"),
|
|
155
|
+
outreachFindContact: (backlinkId) =>
|
|
156
|
+
request("POST", "/api/v1/outreach/find-contact", { backlinkId }),
|
|
157
|
+
outreachQueue: (opts) => request("POST", "/api/v1/outreach/queue", opts),
|
|
158
|
+
outreachDraftReply: (backlinkId) =>
|
|
159
|
+
request("POST", "/api/v1/outreach/draft-reply", { backlinkId }),
|
|
160
|
+
contentIdeas: () => request("GET", "/api/v1/content/ideas"),
|
|
161
|
+
planIdea: (opts) => request("POST", "/api/v1/content/ideas/plan", opts),
|
|
162
|
+
archiveContent: (contentId) =>
|
|
163
|
+
request("POST", "/api/v1/content/archive", { contentId }),
|
|
164
|
+
pageEngagement: () => request("GET", "/api/v1/analytics/engagement"),
|
|
165
|
+
reportSummary: () => request("GET", "/api/v1/reports/summary"),
|
|
166
|
+
reportWins: () => request("GET", "/api/v1/reports/wins"),
|
|
167
|
+
agentActivity: (params = {}) => {
|
|
168
|
+
const q = new URLSearchParams();
|
|
169
|
+
if (params.perAgent) q.set("perAgent", String(params.perAgent));
|
|
170
|
+
if (params.sinceDays) q.set("sinceDays", String(params.sinceDays));
|
|
171
|
+
const qs = q.toString();
|
|
172
|
+
return request("GET", `/api/v1/reports/agent-activity${qs ? `?${qs}` : ""}`);
|
|
173
|
+
},
|
|
174
|
+
backlinks: (status) =>
|
|
175
|
+
request(
|
|
176
|
+
"GET",
|
|
177
|
+
`/api/v1/backlinks${status ? `?status=${encodeURIComponent(status)}` : ""}`
|
|
178
|
+
),
|
|
179
|
+
backlinkStats: () => request("GET", "/api/v1/backlinks/stats"),
|
|
180
|
+
outreachStatus: (backlinkId, status) =>
|
|
181
|
+
request("POST", "/api/v1/outreach/status", { backlinkId, status }),
|
|
182
|
+
linkNetwork: () => request("GET", "/api/v1/link-network"),
|
|
183
|
+
linkNetworkOptIn: (optIn, confirm = false) =>
|
|
184
|
+
request("POST", "/api/v1/link-network/opt-in", { optIn, confirm }),
|
|
185
|
+
linkNetworkRemovePlacement: (placementId, confirm = false) =>
|
|
186
|
+
request("POST", "/api/v1/link-network/remove-placement", {
|
|
187
|
+
placementId,
|
|
188
|
+
confirm,
|
|
189
|
+
}),
|
|
190
|
+
socialThreads: (params = {}) => {
|
|
191
|
+
const q = new URLSearchParams();
|
|
192
|
+
if (params.platform) q.set("platform", params.platform);
|
|
193
|
+
if (params.status) q.set("status", params.status);
|
|
194
|
+
if (params.age) q.set("age", params.age);
|
|
195
|
+
const qs = q.toString();
|
|
196
|
+
return request("GET", `/api/v1/social${qs ? `?${qs}` : ""}`);
|
|
197
|
+
},
|
|
198
|
+
socialStats: () => request("GET", "/api/v1/social/stats"),
|
|
199
|
+
socialStatus: (threadId, status) =>
|
|
200
|
+
request("POST", "/api/v1/social/status", { threadId, status }),
|
|
201
|
+
socialDraftReply: (threadId, mentionMode) =>
|
|
202
|
+
request("POST", "/api/v1/social/draft-reply", { threadId, mentionMode }),
|
|
203
|
+
support: (opts) => request("POST", "/api/v1/support", opts),
|
|
204
|
+
brand: () => request("GET", "/api/v1/brand"),
|
|
205
|
+
brandProfileSet: (opts) => request("POST", "/api/v1/brand/profile", opts),
|
|
206
|
+
brandProductWrite: (opts) => request("POST", "/api/v1/brand/product", opts),
|
|
207
|
+
brandIcpWrite: (opts) => request("POST", "/api/v1/brand/icp", opts),
|
|
106
208
|
};
|
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.6.0" });
|
|
33
33
|
|
|
34
34
|
server.tool(
|
|
35
35
|
"get_overview_funnel",
|
|
@@ -57,11 +57,74 @@ export async function startMcpServer() {
|
|
|
57
57
|
|
|
58
58
|
server.tool(
|
|
59
59
|
"get_visibility_score",
|
|
60
|
-
"
|
|
60
|
+
"Composite visibility score for the workspace: 50% AI citation rate + 30% Google + 20% Bing position-weighted top-10 rank share (top 3 = full credit, 4-10 = half). Returns the composite, weights, and per-pillar subscores (the AI pillar includes a per-model breakdown).",
|
|
61
61
|
{},
|
|
62
62
|
run(() => api.visibilityScore())
|
|
63
63
|
);
|
|
64
64
|
|
|
65
|
+
server.tool(
|
|
66
|
+
"get_citability_report",
|
|
67
|
+
"Published pages ranked by citability score (how extractable each page is for AI answers), worst first, each with its top open fixes. Use to decide which articles to improve.",
|
|
68
|
+
{ limit: z.number().max(100).optional().describe("Max rows, default 25") },
|
|
69
|
+
run(({ limit }) => api.optimizer(limit ?? 25))
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
server.tool(
|
|
73
|
+
"get_citation_sentiment",
|
|
74
|
+
"How AI answers frame the brand when it's cited: positive / neutral / negative counts, daily trend, and recent answer snippets as receipts.",
|
|
75
|
+
{ days: z.number().min(7).max(90).optional().describe("Window in days, default 30") },
|
|
76
|
+
run(({ days }) => api.citationSentiment(days ?? 30))
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
server.tool(
|
|
80
|
+
"get_citation_sources",
|
|
81
|
+
"Which domains AI engines cite when answering the brand's tracked queries, typed brand / competitive / UGC / editorial with counts and share. Shows where citation presence must be earned.",
|
|
82
|
+
{ days: z.number().min(7).max(90).optional().describe("Window in days, default 30") },
|
|
83
|
+
run(({ days }) => api.citationSources(days ?? 30))
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
server.tool(
|
|
87
|
+
"get_share_of_voice",
|
|
88
|
+
"Share of voice vs competitors: the brand's citation rate, top competitor mention rates, and the brand's share of all brand appearances in AI answers over the window.",
|
|
89
|
+
{ days: z.number().min(7).max(90).optional().describe("Window in days, default 30") },
|
|
90
|
+
run(({ days }) => api.shareOfVoice(days ?? 30))
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
server.tool(
|
|
94
|
+
"list_leads",
|
|
95
|
+
"Captured leads with source attribution: email, name, company, status, lead score, and the AI model / query / page that produced each lead.",
|
|
96
|
+
{ limit: z.number().max(500).optional().describe("Max rows, default 100") },
|
|
97
|
+
run(({ limit }) => api.leads(limit ?? 100))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
server.tool(
|
|
101
|
+
"list_tracked_queries",
|
|
102
|
+
"The workspace's tracked queries — the questions RankControl checks weekly across the 6 AI engines for brand citations — with their status and metadata.",
|
|
103
|
+
{},
|
|
104
|
+
run(() => api.trackedQueries())
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
server.tool(
|
|
108
|
+
"get_crawler_access",
|
|
109
|
+
"Daily AI-crawler reachability probe result: whether the site's edge (e.g. Cloudflare Bot Fight Mode) or robots.txt blocks GPTBot/ClaudeBot — a blocked site cannot be read or cited by AI engines.",
|
|
110
|
+
{},
|
|
111
|
+
run(() => api.crawlerAccess())
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
server.tool(
|
|
115
|
+
"add_tracked_query",
|
|
116
|
+
"Add a query to the tracking pool. It starts weekly AI citation checks immediately when a plan slot is free; otherwise it joins the pool untracked.",
|
|
117
|
+
{ queryText: z.string().describe("The search/AI query to track") },
|
|
118
|
+
run(({ queryText }) => api.addQuery(queryText))
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
server.tool(
|
|
122
|
+
"get_traffic_overview",
|
|
123
|
+
"Traffic overview from RankControl's own page analytics: total page views, unique visitors, sessions, bounce rate, and average time on page.",
|
|
124
|
+
{ days: z.enum(["7", "30", "90"]).optional().describe("Window in days, default 30") },
|
|
125
|
+
run(({ days }) => api.trafficOverview(days ? Number(days) : 30))
|
|
126
|
+
);
|
|
127
|
+
|
|
65
128
|
server.tool(
|
|
66
129
|
"get_planning_capacity",
|
|
67
130
|
"How many article titles can still be planned onto the content calendar within the scheduling horizon. Check before plan_content.",
|
|
@@ -69,6 +132,88 @@ export async function startMcpServer() {
|
|
|
69
132
|
run(() => api.planningCapacity())
|
|
70
133
|
);
|
|
71
134
|
|
|
135
|
+
const permLevel = z.enum(["none", "read", "write"]);
|
|
136
|
+
server.tool(
|
|
137
|
+
"get_team",
|
|
138
|
+
"Workspace members and pending invites, with each member's per-screen permission matrix and the seat cap.",
|
|
139
|
+
{},
|
|
140
|
+
run(() => api.team())
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
server.tool(
|
|
144
|
+
"invite_team_member",
|
|
145
|
+
"Invite a member to the workspace with per-screen none/read/write access (unspecified screens default to none — grant at least one). Dry-run unless confirm is true; confirming sends the invite email.",
|
|
146
|
+
{
|
|
147
|
+
email: z.string().describe("Invitee email address"),
|
|
148
|
+
screenPermissions: z
|
|
149
|
+
.object({
|
|
150
|
+
overview: permLevel.optional(),
|
|
151
|
+
content: permLevel.optional(),
|
|
152
|
+
visibility: permLevel.optional(),
|
|
153
|
+
brandControl: permLevel.optional(),
|
|
154
|
+
linkControl: permLevel.optional(),
|
|
155
|
+
analytics: permLevel.optional(),
|
|
156
|
+
reports: permLevel.optional(),
|
|
157
|
+
})
|
|
158
|
+
.optional(),
|
|
159
|
+
confirm: z
|
|
160
|
+
.boolean()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Set true to actually send the invite (default dry-run)"),
|
|
163
|
+
},
|
|
164
|
+
run((args) => api.teamInvite(args))
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
server.tool(
|
|
168
|
+
"revoke_team_invite",
|
|
169
|
+
"Revoke a pending team invite by invitation id (from get_team).",
|
|
170
|
+
{ invitationId: z.string() },
|
|
171
|
+
run(({ invitationId }) => api.teamRevoke(invitationId))
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
server.tool(
|
|
175
|
+
"remove_team_member",
|
|
176
|
+
"Remove a member from the workspace (access revoked immediately). Dry-run unless confirm is true.",
|
|
177
|
+
{
|
|
178
|
+
userId: z.string().describe("Member user id from get_team"),
|
|
179
|
+
confirm: z.boolean().optional(),
|
|
180
|
+
},
|
|
181
|
+
run((args) => api.teamRemove(args))
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
server.tool(
|
|
185
|
+
"list_outreach_prospects",
|
|
186
|
+
"Link outreach pipeline: prospect sites per published article with status (identified → contacted → replied → link placed) and any contact email already found.",
|
|
187
|
+
{},
|
|
188
|
+
run(() => api.outreachProspects())
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
server.tool(
|
|
192
|
+
"find_outreach_contact",
|
|
193
|
+
"Find an outreach email for one prospect by scraping their site (mailto links, contact page, obfuscated text; Prospeo fallback when configured). Returns the stored contact or found:false. Capped at 5/min.",
|
|
194
|
+
{ backlinkId: z.string().describe("Prospect id from list_outreach_prospects") },
|
|
195
|
+
run(({ backlinkId }) => api.outreachFindContact(backlinkId))
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
server.tool(
|
|
199
|
+
"draft_outreach_reply",
|
|
200
|
+
"AI-draft the next reply in an outreach conversation where the prospect has responded (replySnippet present on the prospect). Returns subject/body only — nothing sends; follow with queue_outreach_email to send it.",
|
|
201
|
+
{ backlinkId: z.string().describe("Prospect id from list_outreach_prospects") },
|
|
202
|
+
run(({ backlinkId }) => api.outreachDraftReply(backlinkId))
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
server.tool(
|
|
206
|
+
"queue_outreach_email",
|
|
207
|
+
"Queue an outreach email to a prospect. It sends from the workspace's own connected mailbox, paced ~1 per 10 minutes up to the 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.",
|
|
208
|
+
{
|
|
209
|
+
backlinkId: z.string().describe("Prospect id from list_outreach_prospects"),
|
|
210
|
+
subject: z.string().optional().describe("Override the drafted subject"),
|
|
211
|
+
body: z.string().optional().describe("Override the drafted body"),
|
|
212
|
+
confirm: z.boolean().optional().describe("Set true to actually queue the send"),
|
|
213
|
+
},
|
|
214
|
+
run((args) => api.outreachQueue({ ...args, confirm: args.confirm === true }))
|
|
215
|
+
);
|
|
216
|
+
|
|
72
217
|
server.tool(
|
|
73
218
|
"list_jobs",
|
|
74
219
|
"Recent agent runs (job status feed). Async actions like content planning show up here with their outputs.",
|
|
@@ -117,6 +262,404 @@ export async function startMcpServer() {
|
|
|
117
262
|
run(({ contentId, confirm }) => api.publishContent(contentId, confirm === true))
|
|
118
263
|
);
|
|
119
264
|
|
|
265
|
+
server.tool(
|
|
266
|
+
"generate_article",
|
|
267
|
+
"Write a PLANNED article's body immediately instead of waiting for the scheduled pipeline (generate-only: its publish slot is unchanged). Allowed for past-due slots and up to 3 days ahead. Defaults to a DRY RUN; a human should approve before calling again with confirm=true since generation spends LLM budget.",
|
|
268
|
+
{
|
|
269
|
+
contentId: z.string().describe("The planned content page id"),
|
|
270
|
+
confirm: z.boolean().optional().describe("Set true to actually start generation (default: dry run)"),
|
|
271
|
+
},
|
|
272
|
+
run(({ contentId, confirm }) => api.generateContent(contentId, confirm === true))
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
server.tool(
|
|
276
|
+
"get_article_settings",
|
|
277
|
+
"The workspace article policy: per-article defaults (auto-write, auto-publish, images, title-in-hero, section infographics, Related Reading, YouTube, emojis, link counts, global instructions) plus the flexible-scheduling flag.",
|
|
278
|
+
{},
|
|
279
|
+
run(() => api.articleSettings())
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const articleSettingsShape = {
|
|
283
|
+
autoPublish: z.boolean().optional(),
|
|
284
|
+
autoGenerate: z.boolean().optional().describe("Off = fully manual mode: articles are written only on demand (generate_article) and publish only from the editor"),
|
|
285
|
+
includeInfographics: z.boolean().optional().describe("AI hero + section images"),
|
|
286
|
+
titleInHeroImage: z.boolean().optional().describe("Write the post title into the hero image"),
|
|
287
|
+
includeSectionInfographics: z.boolean().optional().describe("Informational panels in 2,000+ word articles"),
|
|
288
|
+
includeRelatedReading: z.boolean().optional().describe("Related Reading link block at the end"),
|
|
289
|
+
includeYouTube: z.boolean().optional(),
|
|
290
|
+
useEmojis: z.boolean().optional(),
|
|
291
|
+
internalLinksPerArticle: z.number().min(1).max(20).optional(),
|
|
292
|
+
externalLinksPerArticle: z.number().min(0).max(15).optional(),
|
|
293
|
+
globalInstructions: z.string().optional(),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
server.tool(
|
|
297
|
+
"update_article_settings",
|
|
298
|
+
"Merge-patch the article policy: only the fields you pass change. Set flexibleScheduling true to allow 1-5 articles/day and manual rescheduling. styleSet picks the image style set applied to all generated images (heroes, infographic panels, social cards).",
|
|
299
|
+
{
|
|
300
|
+
articleSettings: z.object(articleSettingsShape).optional(),
|
|
301
|
+
flexibleScheduling: z.boolean().optional(),
|
|
302
|
+
styleSet: z
|
|
303
|
+
.enum([
|
|
304
|
+
"classic-editorial",
|
|
305
|
+
"print-craft",
|
|
306
|
+
"storybook-painterly",
|
|
307
|
+
"modern-saas",
|
|
308
|
+
"dark-premium",
|
|
309
|
+
"bold-poster",
|
|
310
|
+
])
|
|
311
|
+
.optional()
|
|
312
|
+
.describe("Image style set for all generated images"),
|
|
313
|
+
},
|
|
314
|
+
run((args) => api.updateArticleSettings(args))
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
server.tool(
|
|
318
|
+
"reschedule_article",
|
|
319
|
+
"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 epoch ms of local midnight.",
|
|
320
|
+
{
|
|
321
|
+
contentId: z.string(),
|
|
322
|
+
targetDayStartMs: z.number().describe("Epoch ms of the target day's local midnight"),
|
|
323
|
+
},
|
|
324
|
+
run(({ contentId, targetDayStartMs }) => api.reschedule(contentId, targetDayStartMs))
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
server.tool(
|
|
328
|
+
"get_internal_links",
|
|
329
|
+
"How the workspace's articles interlink: how many published articles link TO this page (with the source list) and how many internal links it sends out. Recorded from the real injected links at publish render time.",
|
|
330
|
+
{ contentId: z.string().describe("The content page id") },
|
|
331
|
+
run(({ contentId }) => api.internalLinks(contentId))
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
server.tool(
|
|
335
|
+
"list_site_pages",
|
|
336
|
+
"Site pages RankControl links to from inside articles and the Related Reading block (with linkable/alive flags).",
|
|
337
|
+
{},
|
|
338
|
+
run(() => api.sitePages())
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
server.tool(
|
|
342
|
+
"detect_site_links",
|
|
343
|
+
"Scan the customer's sitemap (nested indexes handled) or crawl a blog root page and return same-site page URLs. Costly (3/min); review the URLs, then call add_site_pages with the keepers.",
|
|
344
|
+
{
|
|
345
|
+
source: z.enum(["sitemap", "blogroot"]),
|
|
346
|
+
url: z.string().describe("Sitemap XML URL or a page URL to crawl"),
|
|
347
|
+
},
|
|
348
|
+
run(({ source, url }) => api.detectSiteLinks(source, url))
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
server.tool(
|
|
352
|
+
"add_site_pages",
|
|
353
|
+
"Add site page URLs for internal linking. Duplicates are skipped; page titles fill in automatically in the background.",
|
|
354
|
+
{ urls: z.array(z.string()) },
|
|
355
|
+
run(({ urls }) => api.addSitePages(urls))
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
server.tool(
|
|
359
|
+
"list_repurpose_queue",
|
|
360
|
+
"Published articles with their social repurpose drafts per platform (platform + status chips). Shows which articles still need drafts and whether Postiz is connected.",
|
|
361
|
+
{},
|
|
362
|
+
run(() => api.repurposeQueue())
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
server.tool(
|
|
366
|
+
"get_repurpose_drafts",
|
|
367
|
+
"Full social drafts (title, body, images, status) for one published article across the 8 platforms (LinkedIn, X/Twitter, Pinterest, Instagram, Facebook, Threads, YouTube, TikTok).",
|
|
368
|
+
{ contentId: z.string().describe("The content page id") },
|
|
369
|
+
run(({ contentId }) => api.repurposeDrafts(contentId))
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
server.tool(
|
|
373
|
+
"generate_repurpose_drafts",
|
|
374
|
+
"Draft platform-native social posts for a published article. Regeneration replaces unposted drafts for the chosen platforms. Defaults to a DRY RUN; confirm=true generates (spends LLM budget).",
|
|
375
|
+
{
|
|
376
|
+
contentId: z.string(),
|
|
377
|
+
platforms: z
|
|
378
|
+
.array(
|
|
379
|
+
z.enum([
|
|
380
|
+
"linkedin",
|
|
381
|
+
"twitter",
|
|
382
|
+
"pinterest",
|
|
383
|
+
"instagram",
|
|
384
|
+
"facebook",
|
|
385
|
+
"threads",
|
|
386
|
+
"youtube",
|
|
387
|
+
"tiktok",
|
|
388
|
+
])
|
|
389
|
+
)
|
|
390
|
+
.optional()
|
|
391
|
+
.describe("Subset of platforms; omit for the workspace default"),
|
|
392
|
+
confirm: z.boolean().optional(),
|
|
393
|
+
},
|
|
394
|
+
run((args) => api.repurposeGenerate(args))
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
server.tool(
|
|
398
|
+
"update_repurpose_draft",
|
|
399
|
+
"Edit a repurpose draft's body and/or title before pushing or posting.",
|
|
400
|
+
{
|
|
401
|
+
draftId: z.string(),
|
|
402
|
+
body: z.string().optional(),
|
|
403
|
+
title: z.string().optional(),
|
|
404
|
+
},
|
|
405
|
+
run((args) => api.repurposeEditDraft(args))
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
server.tool(
|
|
409
|
+
"mark_repurpose_posted",
|
|
410
|
+
"Mark a repurpose draft as posted — use after publishing its text manually (outside Postiz).",
|
|
411
|
+
{ draftId: z.string() },
|
|
412
|
+
run(({ draftId }) => api.repurposeMarkPosted(draftId))
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
server.tool(
|
|
416
|
+
"list_postiz_channels",
|
|
417
|
+
"Connected Postiz social channels (id, name, platform). Channel ids are required by push_repurpose_draft.",
|
|
418
|
+
{},
|
|
419
|
+
run(() => api.repurposeChannels())
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
server.tool(
|
|
423
|
+
"push_repurpose_draft",
|
|
424
|
+
"Send a repurpose draft to connected Postiz channels (post now, schedule, or save as a Postiz draft). PUBLICLY VISIBLE side effect: defaults to a DRY RUN; a human should approve before calling again with confirm=true.",
|
|
425
|
+
{
|
|
426
|
+
draftId: z.string(),
|
|
427
|
+
integrationIds: z
|
|
428
|
+
.array(z.string())
|
|
429
|
+
.optional()
|
|
430
|
+
.describe(
|
|
431
|
+
"Postiz channel ids from list_postiz_channels (required with confirm)"
|
|
432
|
+
),
|
|
433
|
+
scheduleType: z
|
|
434
|
+
.enum(["now", "schedule", "draft"])
|
|
435
|
+
.optional()
|
|
436
|
+
.describe("Default now"),
|
|
437
|
+
date: z.string().optional().describe("ISO time for scheduleType=schedule"),
|
|
438
|
+
confirm: z.boolean().optional(),
|
|
439
|
+
},
|
|
440
|
+
run((args) => api.repurposePush(args))
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
server.tool(
|
|
444
|
+
"get_content_ideas",
|
|
445
|
+
"Scored content-idea backlog: uncovered tracked queries ranked by citation gaps, competitor citations, trending topics, and quick-win rankings. Each idea carries a suggested title and winnability metrics.",
|
|
446
|
+
{},
|
|
447
|
+
run(() => api.contentIdeas())
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
server.tool(
|
|
451
|
+
"plan_content_idea",
|
|
452
|
+
"Put a content idea on the calendar as a planned article. Pass the idea's queryText (and its suggested title if you have it — otherwise a title is generated).",
|
|
453
|
+
{
|
|
454
|
+
queryText: z.string().describe("The idea's keyword/query text"),
|
|
455
|
+
title: z.string().optional().describe("Exact headline to plan"),
|
|
456
|
+
},
|
|
457
|
+
run((args) => api.planIdea(args))
|
|
458
|
+
);
|
|
459
|
+
|
|
460
|
+
server.tool(
|
|
461
|
+
"archive_content",
|
|
462
|
+
"Archive an article, removing it from the working set. Reversible in the dashboard.",
|
|
463
|
+
{ contentId: z.string() },
|
|
464
|
+
run(({ contentId }) => api.archiveContent(contentId))
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
server.tool(
|
|
468
|
+
"get_page_engagement",
|
|
469
|
+
"Per-page engagement for the last 30 days: views, AI citations, and a view sparkline per published page.",
|
|
470
|
+
{},
|
|
471
|
+
run(() => api.pageEngagement())
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
server.tool(
|
|
475
|
+
"get_exec_summary",
|
|
476
|
+
"Executive report: last 30 days vs the 30 before — cite rate, citations, page views, AI-referred views, crawler hits, and articles published.",
|
|
477
|
+
{},
|
|
478
|
+
run(() => api.reportSummary())
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
server.tool(
|
|
482
|
+
"get_top_wins",
|
|
483
|
+
"Biggest wins of the last 30 days: most-cited page, best query cite-rate jump, biggest Google ranking climb, best new backlink.",
|
|
484
|
+
{},
|
|
485
|
+
run(() => api.reportWins())
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
server.tool(
|
|
489
|
+
"get_agent_activity",
|
|
490
|
+
"Recent pipeline runs per agent lane (brand control, radar, forge, recon, deploy, outreach, sentinel, social).",
|
|
491
|
+
{
|
|
492
|
+
perAgent: z.number().max(50).optional().describe("Runs per lane, default 15"),
|
|
493
|
+
sinceDays: z.number().optional().describe("Window in days, default 30"),
|
|
494
|
+
},
|
|
495
|
+
run((args) => api.agentActivity(args))
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
server.tool(
|
|
499
|
+
"list_backlinks",
|
|
500
|
+
"The workspace's backlink table, newest first.",
|
|
501
|
+
{
|
|
502
|
+
status: z
|
|
503
|
+
.string()
|
|
504
|
+
.optional()
|
|
505
|
+
.describe(
|
|
506
|
+
"Filter: discovered, verified, lost, identified, contacted, replied, link_placed, rejected"
|
|
507
|
+
),
|
|
508
|
+
},
|
|
509
|
+
run(({ status }) => api.backlinks(status))
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
server.tool(
|
|
513
|
+
"get_backlink_stats",
|
|
514
|
+
"Backlink totals (live, lost, avg DA, unique domains) plus outreach pipeline counts.",
|
|
515
|
+
{},
|
|
516
|
+
run(() => api.backlinkStats())
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
server.tool(
|
|
520
|
+
"update_outreach_status",
|
|
521
|
+
"Move an outreach prospect through the pipeline.",
|
|
522
|
+
{
|
|
523
|
+
backlinkId: z.string(),
|
|
524
|
+
status: z.enum(["identified", "contacted", "replied", "link_placed", "rejected"]),
|
|
525
|
+
},
|
|
526
|
+
run(({ backlinkId, status }) => api.outreachStatus(backlinkId, status))
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
server.tool(
|
|
530
|
+
"get_link_network",
|
|
531
|
+
"Link Network state: credit balance/earn/spend, membership (optedIn), and hosted/received placements with partners and anchors.",
|
|
532
|
+
{},
|
|
533
|
+
run(() => api.linkNetwork())
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
server.tool(
|
|
537
|
+
"set_link_network_opt_in",
|
|
538
|
+
"Join or leave the Link Network. SIDE EFFECT on partner sites (leaving retires live links): defaults to a DRY RUN; a human should approve before calling again with confirm=true.",
|
|
539
|
+
{ optIn: z.boolean(), confirm: z.boolean().optional() },
|
|
540
|
+
run(({ optIn, confirm }) => api.linkNetworkOptIn(optIn, confirm))
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
server.tool(
|
|
544
|
+
"remove_network_placement",
|
|
545
|
+
"Retire one Link Network placement. VISIBLE on the partner site: defaults to a DRY RUN; call again with confirm=true after human approval.",
|
|
546
|
+
{ placementId: z.string(), confirm: z.boolean().optional() },
|
|
547
|
+
run(({ placementId, confirm }) => api.linkNetworkRemovePlacement(placementId, confirm))
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
server.tool(
|
|
551
|
+
"list_social_threads",
|
|
552
|
+
"Social thread prospects (Reddit/X) where the brand's articles could earn a mention, with subreddit rules context, sorted by intent.",
|
|
553
|
+
{
|
|
554
|
+
platform: z.enum(["reddit", "twitter"]).optional(),
|
|
555
|
+
status: z.string().optional(),
|
|
556
|
+
age: z.enum(["ranked", "new"]).optional(),
|
|
557
|
+
},
|
|
558
|
+
run((args) => api.socialThreads(args))
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
server.tool(
|
|
562
|
+
"get_social_stats",
|
|
563
|
+
"Social thread pipeline counts.",
|
|
564
|
+
{},
|
|
565
|
+
run(() => api.socialStats())
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
server.tool(
|
|
569
|
+
"update_social_thread_status",
|
|
570
|
+
"Move a social thread through the pipeline (e.g. dismissed, replied).",
|
|
571
|
+
{ threadId: z.string(), status: z.string() },
|
|
572
|
+
run(({ threadId, status }) => api.socialStatus(threadId, status))
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
server.tool(
|
|
576
|
+
"draft_social_reply",
|
|
577
|
+
"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.",
|
|
578
|
+
{
|
|
579
|
+
threadId: z.string(),
|
|
580
|
+
mentionMode: z
|
|
581
|
+
.enum(["none", "natural", "founderOpen"])
|
|
582
|
+
.optional()
|
|
583
|
+
.describe("Brand-mention mode; warm-up accounts are clamped to none"),
|
|
584
|
+
},
|
|
585
|
+
run(({ threadId, mentionMode }) => api.socialDraftReply(threadId, mentionMode))
|
|
586
|
+
);
|
|
587
|
+
|
|
588
|
+
server.tool(
|
|
589
|
+
"contact_support",
|
|
590
|
+
"Send a message or bug report to the RankControl team (emails support with the workspace identified; the org owner is set as reply-to). Use when an API call errors unexpectedly or data looks wrong — include the failing request, ids, and what you expected. Capped at 3/min.",
|
|
591
|
+
{
|
|
592
|
+
subject: z.string(),
|
|
593
|
+
message: z.string().describe("Details: what happened, errors, ids, steps to reproduce"),
|
|
594
|
+
pageUrl: z.string().optional().describe("Related dashboard or API URL"),
|
|
595
|
+
},
|
|
596
|
+
run((args) => api.support(args))
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
server.tool(
|
|
600
|
+
"get_brand",
|
|
601
|
+
"The workspace's brand data in one read: brand profile (voice, colors, meta), links/CTAs, products, and buyer profiles (ICPs). This is the context RankControl's writers use.",
|
|
602
|
+
{},
|
|
603
|
+
run(() => api.brand())
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
server.tool(
|
|
607
|
+
"update_brand_profile",
|
|
608
|
+
"Patch brand voice/style fields: tone, colors, fontFamily, metaTitle/Description/Keywords, defaultLocale, flexibleScheduling. Only provided fields change; they steer all future article generation.",
|
|
609
|
+
{
|
|
610
|
+
tone: z.string().optional(),
|
|
611
|
+
primaryColor: z.string().optional(),
|
|
612
|
+
secondaryColor: z.string().optional(),
|
|
613
|
+
fontFamily: z.string().optional(),
|
|
614
|
+
metaTitle: z.string().optional(),
|
|
615
|
+
metaDescription: z.string().optional(),
|
|
616
|
+
metaKeywords: z.string().optional(),
|
|
617
|
+
defaultLocale: z.string().optional().describe("BCP-47 tag from the supported registry"),
|
|
618
|
+
flexibleScheduling: z.boolean().optional(),
|
|
619
|
+
},
|
|
620
|
+
run((args) => api.brandProfileSet(args))
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
server.tool(
|
|
624
|
+
"write_brand_product",
|
|
625
|
+
"Create, update, or delete a product in brand memory (feeds article context + entity triples). Create: name+description+category. Update: productId + changed fields. Delete: productId + del=true.",
|
|
626
|
+
{
|
|
627
|
+
productId: z.string().optional(),
|
|
628
|
+
del: z.boolean().optional(),
|
|
629
|
+
name: z.string().optional(),
|
|
630
|
+
description: z.string().optional(),
|
|
631
|
+
url: z.string().optional(),
|
|
632
|
+
features: z.array(z.string()).optional(),
|
|
633
|
+
pricing: z.string().optional(),
|
|
634
|
+
category: z.string().optional(),
|
|
635
|
+
usps: z.array(z.string()).optional(),
|
|
636
|
+
isActive: z.boolean().optional(),
|
|
637
|
+
},
|
|
638
|
+
run((args) => api.brandProductWrite(args))
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
server.tool(
|
|
642
|
+
"write_brand_icp",
|
|
643
|
+
"Create, update, or delete a buyer profile / ICP (feeds article targeting + entity triples). Create: title+industry+demographics. Update: profileId + changed fields. Delete: profileId + del=true.",
|
|
644
|
+
{
|
|
645
|
+
profileId: z.string().optional(),
|
|
646
|
+
del: z.boolean().optional(),
|
|
647
|
+
title: z.string().optional(),
|
|
648
|
+
industry: z.string().optional(),
|
|
649
|
+
companySize: z.string().optional(),
|
|
650
|
+
painPoints: z.array(z.string()).optional(),
|
|
651
|
+
buyingTriggers: z.array(z.string()).optional(),
|
|
652
|
+
objections: z.array(z.string()).optional(),
|
|
653
|
+
preferredChannels: z.array(z.string()).optional(),
|
|
654
|
+
demographics: z
|
|
655
|
+
.record(z.string(), z.unknown())
|
|
656
|
+
.optional()
|
|
657
|
+
.describe("Demographics object; required on create (same shape the dashboard saves)"),
|
|
658
|
+
isActive: z.boolean().optional(),
|
|
659
|
+
},
|
|
660
|
+
run((args) => api.brandIcpWrite(args))
|
|
661
|
+
);
|
|
662
|
+
|
|
120
663
|
await server.connect(new StdioServerTransport());
|
|
121
664
|
// Keep the process alive; the transport owns stdin/stdout from here
|
|
122
665
|
console.error("rankcontrol MCP server running on stdio");
|