rankcontrol 0.6.0 → 0.8.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/LICENSE +1 -1
- package/README.md +163 -38
- package/package.json +1 -1
- package/src/cli.mjs +164 -12
- package/src/client.mjs +33 -1
- package/src/login.mjs +10 -3
- package/src/mcp.mjs +192 -10
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -9,15 +9,15 @@ Every command calls the same authed, rate-limited, org-scoped API the RankContro
|
|
|
9
9
|
|
|
10
10
|
## Auth
|
|
11
11
|
|
|
12
|
-
The easy way
|
|
12
|
+
The easy way (browser approval; you must be logged in to RankControl):
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
15
|
npx rankcontrol login
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
That opens your dashboard, you check the confirmation code matches
|
|
18
|
+
That opens your dashboard, you check the confirmation code matches and approve. The CLI stores a key in `~/.rankcontrol/config.json`. `npx rankcontrol logout` removes it (revoke the key itself in Settings → API).
|
|
19
19
|
|
|
20
|
-
The headless way (CI, servers, MCP `env` blocks)
|
|
20
|
+
The headless way (CI, servers, MCP `env` blocks): create a key in **RankControl → Settings → API**, then:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
export RANKCONTROL_API_KEY=rctrl_pk_...
|
|
@@ -28,40 +28,101 @@ The env var wins over the stored config when both exist. Scopes are set per key;
|
|
|
28
28
|
## CLI
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
31
|
+
# Visibility and citations
|
|
32
|
+
npx rankcontrol funnel # AI crawls / AI visits (30d)
|
|
32
33
|
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
|
|
35
34
|
npx rankcontrol visibility --days 60 # daily AI visibility score trend
|
|
36
35
|
npx rankcontrol sov --days 30 # share of voice vs competitors
|
|
37
36
|
npx rankcontrol sources --days 30 # cited domains typed brand/competitive/ugc/editorial
|
|
38
37
|
npx rankcontrol sentiment --days 30 # how AI answers frame your brand
|
|
39
|
-
npx rankcontrol optimizer --limit 25 # pages ranked by citability, worst first
|
|
40
38
|
npx rankcontrol citations --model chatgpt
|
|
39
|
+
npx rankcontrol crawler-access # can AI crawlers reach your site (daily probe)
|
|
41
40
|
npx rankcontrol queries # tracked queries checked weekly across AI engines
|
|
41
|
+
npx rankcontrol query-add "best crm for agencies"
|
|
42
|
+
npx rankcontrol query-edit <queryId> "new text"
|
|
43
|
+
npx rankcontrol query-track <queryId> off # pause weekly checks (frees a slot)
|
|
44
|
+
npx rankcontrol query-remove <queryId>
|
|
45
|
+
npx rankcontrol competitors
|
|
46
|
+
npx rankcontrol competitor-add "Acme" https://acme.com
|
|
47
|
+
npx rankcontrol competitor-remove <competitorId>
|
|
48
|
+
|
|
49
|
+
# Content pipeline
|
|
42
50
|
npx rankcontrol capacity # remaining content-plan slots
|
|
51
|
+
npx rankcontrol content # pages with status
|
|
43
52
|
npx rankcontrol plan-content --topics "bus travel" --max-difficulty 40
|
|
44
53
|
npx rankcontrol commit-titles --file titles.json
|
|
45
|
-
npx rankcontrol
|
|
46
|
-
npx rankcontrol
|
|
47
|
-
npx rankcontrol
|
|
48
|
-
npx rankcontrol generate <contentId> --confirm # write a planned article now
|
|
54
|
+
npx rankcontrol ideas # scored content-idea backlog
|
|
55
|
+
npx rankcontrol plan-idea "query text" # put an idea on the calendar
|
|
56
|
+
npx rankcontrol delete-planned <contentId> # title returns to the idea pool
|
|
57
|
+
npx rankcontrol generate <contentId> --confirm # write a planned article now
|
|
58
|
+
npx rankcontrol publish <contentId> --confirm # dry run without --confirm
|
|
59
|
+
npx rankcontrol archive <contentId>
|
|
60
|
+
npx rankcontrol reschedule <contentId> 2026-08-04
|
|
49
61
|
npx rankcontrol settings # article policy (auto-write, auto-publish, images, ...)
|
|
50
62
|
npx rankcontrol settings-set --auto-generate off --auto-publish off
|
|
51
|
-
npx rankcontrol
|
|
63
|
+
npx rankcontrol topics # pillar list (topic clusters)
|
|
64
|
+
npx rankcontrol topics-set "Topic A" "Topic B" # replaces the FULL list
|
|
52
65
|
npx rankcontrol internal-links <contentId> # who links here / links out
|
|
53
66
|
npx rankcontrol site-pages # pages used for internal links
|
|
54
67
|
npx rankcontrol detect-links https://example.com/sitemap.xml
|
|
55
68
|
npx rankcontrol add-pages https://example.com/pricing
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
|
|
70
|
+
# Leads and analytics
|
|
71
|
+
npx rankcontrol traffic --days 30 # page views, visitors, bounce rate, time on page
|
|
72
|
+
npx rankcontrol engagement # per-page views, citations, sparkline
|
|
73
|
+
npx rankcontrol analytics-sources # which source writes traffic/crawler data
|
|
74
|
+
npx rankcontrol analytics-set-source <dataType> <source>
|
|
75
|
+
npx rankcontrol analytics-activate <screen>
|
|
76
|
+
|
|
77
|
+
# Reports
|
|
78
|
+
npx rankcontrol report # exec summary: last 30 days vs the 30 before
|
|
79
|
+
npx rankcontrol wins # biggest wins of the last 30 days
|
|
80
|
+
npx rankcontrol agent-activity # recent runs per agent lane
|
|
81
|
+
npx rankcontrol jobs # async job status
|
|
82
|
+
|
|
83
|
+
# Backlinks and outreach
|
|
84
|
+
npx rankcontrol backlinks --status live
|
|
85
|
+
npx rankcontrol backlink-stats
|
|
86
|
+
npx rankcontrol outreach-prospects
|
|
87
|
+
npx rankcontrol outreach-find-contact <backlinkId>
|
|
88
|
+
npx rankcontrol outreach-draft-reply <backlinkId>
|
|
89
|
+
npx rankcontrol outreach-queue <backlinkId> --confirm # sends from YOUR mailbox
|
|
90
|
+
npx rankcontrol outreach-status <backlinkId> <status>
|
|
91
|
+
npx rankcontrol network # Link Network credits + placements
|
|
92
|
+
npx rankcontrol network-opt-in on --confirm
|
|
93
|
+
npx rankcontrol network-remove-placement <placementId> --confirm
|
|
94
|
+
|
|
95
|
+
# Social and repurposing
|
|
96
|
+
npx rankcontrol repurpose [contentId] # published articles + social drafts (8 platforms)
|
|
58
97
|
npx rankcontrol repurpose-generate <contentId> --platforms twitter --confirm
|
|
59
98
|
npx rankcontrol repurpose-edit <draftId> --title "New hook"
|
|
60
|
-
npx rankcontrol repurpose-channels # Postiz channel ids
|
|
61
|
-
npx rankcontrol repurpose-push <draftId> --channels <id> # dry run
|
|
99
|
+
npx rankcontrol repurpose-channels # Postiz channel ids
|
|
62
100
|
npx rankcontrol repurpose-push <draftId> --channels <id> --confirm
|
|
63
|
-
npx rankcontrol repurpose-mark-posted <draftId>
|
|
64
|
-
npx rankcontrol
|
|
101
|
+
npx rankcontrol repurpose-mark-posted <draftId>
|
|
102
|
+
npx rankcontrol social # Reddit/X threads where your articles fit
|
|
103
|
+
npx rankcontrol social-stats
|
|
104
|
+
npx rankcontrol social-status <threadId> <status>
|
|
105
|
+
npx rankcontrol social-draft-reply <threadId>
|
|
106
|
+
|
|
107
|
+
# Brand, team, workspace
|
|
108
|
+
npx rankcontrol brand # brand profile, products, ICPs in one read
|
|
109
|
+
npx rankcontrol brand-set --industry "SaaS" # name, industry, description, authors, style refs
|
|
110
|
+
npx rankcontrol brand-profile-set --json '{"tone":"confident"}'
|
|
111
|
+
npx rankcontrol brand-product --json @product.json
|
|
112
|
+
npx rankcontrol brand-icp --json @icp.json
|
|
113
|
+
npx rankcontrol team
|
|
114
|
+
npx rankcontrol team-invite user@company.com --perms content=write,analytics=read --confirm
|
|
115
|
+
npx rankcontrol team-revoke <invitationId>
|
|
116
|
+
npx rankcontrol team-remove <memberId> --confirm
|
|
117
|
+
npx rankcontrol support "Subject" --message "..."
|
|
118
|
+
|
|
119
|
+
# Integration helpers
|
|
120
|
+
npx rankcontrol shopify-install-url <shop>
|
|
121
|
+
npx rankcontrol cloudflare-connect # OAuth URL for read-only crawler analytics
|
|
122
|
+
npx rankcontrol cloudflare-zones
|
|
123
|
+
npx rankcontrol cloudflare-zone <zoneId> <zoneName>
|
|
124
|
+
npx rankcontrol framer-install-embed --confirm
|
|
125
|
+
npx rankcontrol webflow-custom-code --confirm
|
|
65
126
|
```
|
|
66
127
|
|
|
67
128
|
## MCP
|
|
@@ -86,39 +147,103 @@ Or in `.mcp.json` / Cursor `mcp.json`:
|
|
|
86
147
|
}
|
|
87
148
|
```
|
|
88
149
|
|
|
89
|
-
### Tools
|
|
150
|
+
### Tools (83)
|
|
151
|
+
|
|
152
|
+
**Visibility and citations**
|
|
90
153
|
|
|
91
154
|
| Tool | What it does |
|
|
92
155
|
|---|---|
|
|
93
|
-
| `get_overview_funnel` | AI crawls / AI visits
|
|
94
|
-
| `list_leads` | Captured leads with AI model / query / page attribution |
|
|
95
|
-
| `get_traffic_overview` | Page views, visitors, sessions, bounce rate, time on page |
|
|
96
|
-
| `get_visibility_trend` | Daily visibility score from stored weekly citation checks |
|
|
97
|
-
| `list_citations` | Recent citation checks per AI model |
|
|
98
|
-
| `list_tracked_queries` | The queries checked weekly across the AI engines |
|
|
156
|
+
| `get_overview_funnel` | AI crawls / AI visits, last 30 days |
|
|
99
157
|
| `get_visibility_score` | Composite score (50% AI + 30% Google + 20% Bing) with subscores |
|
|
158
|
+
| `get_visibility_trend` | Daily visibility score from stored weekly citation checks |
|
|
100
159
|
| `get_share_of_voice` | Your citation rate vs competitor mention rates + your share |
|
|
101
160
|
| `get_citation_sources` | Cited domains typed brand / competitive / UGC / editorial |
|
|
102
161
|
| `get_citation_sentiment` | Positive / neutral / negative framing with recent receipts |
|
|
103
162
|
| `get_citability_report` | Published pages by citability score, worst first, with fixes |
|
|
163
|
+
| `list_citations` | Recent citation checks per AI model |
|
|
164
|
+
| `get_crawler_access` | Daily AI-crawler reachability probe (edge blocks, robots.txt) |
|
|
165
|
+
| `list_tracked_queries` | The queries checked weekly across the AI engines |
|
|
166
|
+
| `add_tracked_query` / `update_tracked_query` / `set_query_tracking` / `remove_tracked_query` | Manage the query pool and its plan slots |
|
|
167
|
+
| `get_competitors` / `add_competitor` / `remove_competitor` | Manage share-of-voice competitors (max 10) |
|
|
168
|
+
|
|
169
|
+
**Content pipeline**
|
|
170
|
+
|
|
171
|
+
| Tool | What it does |
|
|
172
|
+
|---|---|
|
|
104
173
|
| `get_planning_capacity` | Remaining calendar slots (check before planning) |
|
|
105
174
|
| `plan_content` | Generate candidate titles (capacity-gated, nothing scheduled) |
|
|
106
175
|
| `commit_planned_titles` | Approve reviewed titles onto the calendar |
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
176
|
+
| `get_content_ideas` / `plan_content_idea` | Scored idea backlog; put an idea on the calendar |
|
|
177
|
+
| `delete_planned_title` | Remove a planned title (returns to the idea pool) |
|
|
178
|
+
| `generate_article` | Write a planned article's body now; dry run by default |
|
|
179
|
+
| `publish_content` | Dry run by default; `confirm: true` publishes to the CMS |
|
|
180
|
+
| `archive_content` | Archive an article (reversible in the dashboard) |
|
|
110
181
|
| `reschedule_article` | Move a planned article to another day (flexible schedule) |
|
|
182
|
+
| `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) |
|
|
111
184
|
| `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 |
|
|
185
|
+
| `list_site_pages` / `add_site_pages` / `detect_site_links` | Pages used for in-article internal links |
|
|
121
186
|
| `list_content` | Content pages with status |
|
|
187
|
+
|
|
188
|
+
**Leads and analytics**
|
|
189
|
+
|
|
190
|
+
| Tool | What it does |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `get_traffic_overview` | Page views, visitors, sessions, bounce rate, time on page |
|
|
193
|
+
| `get_page_engagement` | Per-page views, citations, and a view sparkline |
|
|
194
|
+
| `get_analytics_sources` / `set_analytics_source` | Which source writes traffic and crawler data |
|
|
195
|
+
| `activate_analytics_screen` | One-time activation of the Analytics or Reports screen |
|
|
196
|
+
|
|
197
|
+
**Reports**
|
|
198
|
+
|
|
199
|
+
| Tool | What it does |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `get_exec_summary` | Last 30 days vs the 30 before: cite rate, views, crawls |
|
|
202
|
+
| `get_top_wins` | Most-cited page, best cite-rate jump, biggest ranking climb |
|
|
203
|
+
| `get_agent_activity` | Recent pipeline runs per agent lane |
|
|
122
204
|
| `list_jobs` | Async job status (agent runs) |
|
|
123
205
|
|
|
124
|
-
|
|
206
|
+
**Backlinks and outreach**
|
|
207
|
+
|
|
208
|
+
| Tool | What it does |
|
|
209
|
+
|---|---|
|
|
210
|
+
| `list_backlinks` / `get_backlink_stats` | Backlink table and totals |
|
|
211
|
+
| `list_outreach_prospects` | Outreach pipeline per published article |
|
|
212
|
+
| `find_outreach_contact` | Scrape a prospect site for an outreach email |
|
|
213
|
+
| `draft_outreach_reply` | AI-draft the next reply in a live conversation |
|
|
214
|
+
| `queue_outreach_email` | Queue an email from the workspace's own mailbox |
|
|
215
|
+
| `update_outreach_status` | Move a prospect through the pipeline |
|
|
216
|
+
| `get_link_network` | Credit balance, membership, placements |
|
|
217
|
+
| `set_link_network_opt_in` / `remove_network_placement` | Join/leave, retire placements; dry run by default |
|
|
218
|
+
|
|
219
|
+
**Social and repurposing**
|
|
220
|
+
|
|
221
|
+
| Tool | What it does |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `list_repurpose_queue` / `get_repurpose_drafts` | Published articles + social drafts (8 platforms) |
|
|
224
|
+
| `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 |
|
|
226
|
+
| `mark_repurpose_posted` | Record a draft as posted manually |
|
|
227
|
+
| `list_social_threads` / `get_social_stats` | Reddit/X threads where your articles fit |
|
|
228
|
+
| `update_social_thread_status` / `draft_social_reply` | Work a thread; drafts never auto-post |
|
|
229
|
+
|
|
230
|
+
**Brand, team, workspace**
|
|
231
|
+
|
|
232
|
+
| Tool | What it does |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `get_brand` | Brand profile, links/CTAs, products, and ICPs in one read |
|
|
235
|
+
| `update_brand_identity` | Name, industry, description, authors (EEAT bylines), style refs |
|
|
236
|
+
| `update_brand_profile` | Voice, colors, meta fields, locale |
|
|
237
|
+
| `write_brand_product` / `write_brand_icp` | Create/update/delete products and buyer profiles |
|
|
238
|
+
| `get_team` / `invite_team_member` / `revoke_team_invite` / `remove_team_member` | Seats, invites, per-screen permissions |
|
|
239
|
+
| `contact_support` | Message the RankControl team with the workspace identified |
|
|
240
|
+
|
|
241
|
+
**Integration helpers**
|
|
242
|
+
|
|
243
|
+
| Tool | What it does |
|
|
244
|
+
|---|---|
|
|
245
|
+
| `shopify_install_url` | Mint an install link for the Shopify app |
|
|
246
|
+
| `get_cloudflare_connect_url` / `list_cloudflare_zones` / `select_cloudflare_zone` | Connect read-only Cloudflare crawler analytics |
|
|
247
|
+
| `install_framer_embed` / `install_webflow_custom_code` | Install visit tracking; dry run by default |
|
|
248
|
+
|
|
249
|
+
The plan→commit split is intentional: `plan_content` proposes, a human (or a supervising agent) reviews, `commit_planned_titles` approves. `publish_content`, `generate_article`, and everything with a public side effect are dry-run-first for the same reason.
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -33,11 +33,10 @@ export function runCli(argv) {
|
|
|
33
33
|
|
|
34
34
|
program
|
|
35
35
|
.command("login")
|
|
36
|
-
.description("Authenticate via your browser (
|
|
37
|
-
.
|
|
38
|
-
.action(async (opts) => {
|
|
36
|
+
.description("Authenticate via your browser (full workspace access; billing stays in the dashboard)")
|
|
37
|
+
.action(async () => {
|
|
39
38
|
const { login } = await import("./login.mjs");
|
|
40
|
-
await login(
|
|
39
|
+
await login().catch(fail);
|
|
41
40
|
});
|
|
42
41
|
|
|
43
42
|
program
|
|
@@ -50,15 +49,9 @@ export function runCli(argv) {
|
|
|
50
49
|
|
|
51
50
|
program
|
|
52
51
|
.command("funnel")
|
|
53
|
-
.description("AI pipeline last 30 days: crawls, AI visits
|
|
52
|
+
.description("AI pipeline last 30 days: crawls, AI visits")
|
|
54
53
|
.action(() => api.overviewFunnel().then(out).catch(fail));
|
|
55
54
|
|
|
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
55
|
program
|
|
63
56
|
.command("traffic")
|
|
64
57
|
.description("Traffic overview: page views, visitors, sessions, bounce rate, time on page")
|
|
@@ -119,15 +112,154 @@ export function runCli(argv) {
|
|
|
119
112
|
|
|
120
113
|
program
|
|
121
114
|
.command("crawler-access")
|
|
122
|
-
.description("Daily AI-crawler reachability probe: is the site's edge or robots.txt blocking GPTBot/ClaudeBot")
|
|
115
|
+
.description("Daily AI-crawler reachability probe (a diagnostic, not a metric): is the site's edge or robots.txt blocking GPTBot/ClaudeBot")
|
|
123
116
|
.action(() => api.crawlerAccess().then(out).catch(fail));
|
|
124
117
|
|
|
118
|
+
program
|
|
119
|
+
.command("analytics-sources")
|
|
120
|
+
.description("Per-org analytics sources: selections, qualified options, Cloudflare/plugin/embed install state")
|
|
121
|
+
.action(() => api.analyticsSources().then(out).catch(fail));
|
|
122
|
+
|
|
123
|
+
program
|
|
124
|
+
.command("analytics-set-source")
|
|
125
|
+
.description("Select the writer for a data type (validated against what the setup supports)")
|
|
126
|
+
.argument("<dataType>", "humanTraffic or aiCrawlers")
|
|
127
|
+
.argument("<source>", "embed, wordpress_plugin, cloudflare or none")
|
|
128
|
+
.action((dataType, source) =>
|
|
129
|
+
api.setAnalyticsSource(dataType, source).then(out).catch(fail)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
program
|
|
133
|
+
.command("analytics-activate")
|
|
134
|
+
.description("Turn on the Analytics or Reports screen for this workspace")
|
|
135
|
+
.argument("<screen>", "analytics or reports")
|
|
136
|
+
.action((screen) => api.activateScreen(screen).then(out).catch(fail));
|
|
137
|
+
|
|
138
|
+
program
|
|
139
|
+
.command("cloudflare-connect")
|
|
140
|
+
.description("Print the Cloudflare OAuth URL to connect read-only crawler analytics")
|
|
141
|
+
.action(() =>
|
|
142
|
+
api
|
|
143
|
+
.cloudflareConnectUrl()
|
|
144
|
+
.then((d) => out(d?.url ?? d))
|
|
145
|
+
.catch(fail)
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
program
|
|
149
|
+
.command("cloudflare-zones")
|
|
150
|
+
.description("List Cloudflare zones visible to the connected grant")
|
|
151
|
+
.action(() => api.cloudflareZones().then(out).catch(fail));
|
|
152
|
+
|
|
153
|
+
program
|
|
154
|
+
.command("cloudflare-zone")
|
|
155
|
+
.description("Pick the polled zone (also selects Cloudflare as the AI-crawler source and starts the first sync)")
|
|
156
|
+
.argument("<zoneId>", "Zone id from cloudflare-zones")
|
|
157
|
+
.argument("<zoneName>", "Zone name, e.g. example.com")
|
|
158
|
+
.action((zoneId, zoneName) =>
|
|
159
|
+
api.cloudflareSelectZone(zoneId, zoneName).then(out).catch(fail)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
program
|
|
163
|
+
.command("framer-install-embed")
|
|
164
|
+
.description("Install site-wide tracking on the Framer project (dry run without --confirm; installing also publishes the Framer site)")
|
|
165
|
+
.option("--integration <id>", "Integration id (defaults to the org's Framer integration)")
|
|
166
|
+
.option("--confirm", "Actually install", false)
|
|
167
|
+
.action((opts) =>
|
|
168
|
+
api
|
|
169
|
+
.framerInstallEmbed({
|
|
170
|
+
...(opts.integration ? { integrationId: opts.integration } : {}),
|
|
171
|
+
confirm: Boolean(opts.confirm),
|
|
172
|
+
})
|
|
173
|
+
.then(out)
|
|
174
|
+
.catch(fail)
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
program
|
|
178
|
+
.command("webflow-custom-code")
|
|
179
|
+
.description("Install the tracking loader via Webflow's Custom Code API (dry run without --confirm; live on the customer's next site publish)")
|
|
180
|
+
.option("--integration <id>", "Integration id (defaults to the org's Webflow integration)")
|
|
181
|
+
.option("--token <token>", "New site token with CMS + Custom code scopes (replaces the stored one)")
|
|
182
|
+
.option("--confirm", "Actually install", false)
|
|
183
|
+
.action((opts) =>
|
|
184
|
+
api
|
|
185
|
+
.webflowInstallCustomCode({
|
|
186
|
+
...(opts.integration ? { integrationId: opts.integration } : {}),
|
|
187
|
+
...(opts.token ? { apiToken: opts.token } : {}),
|
|
188
|
+
confirm: Boolean(opts.confirm),
|
|
189
|
+
})
|
|
190
|
+
.then(out)
|
|
191
|
+
.catch(fail)
|
|
192
|
+
);
|
|
193
|
+
|
|
125
194
|
program
|
|
126
195
|
.command("query-add")
|
|
127
196
|
.description("Add a query to the tracking pool (tracks immediately when a plan slot is free)")
|
|
128
197
|
.argument("<queryText>", "The search/AI query to track")
|
|
129
198
|
.action((queryText) => api.addQuery(queryText).then(out).catch(fail));
|
|
130
199
|
|
|
200
|
+
program
|
|
201
|
+
.command("query-remove")
|
|
202
|
+
.description("Delete a query from the tracking pool")
|
|
203
|
+
.argument("<queryId>", "Query id (from the queries command)")
|
|
204
|
+
.action((queryId) => api.removeQuery(queryId).then(out).catch(fail));
|
|
205
|
+
|
|
206
|
+
program
|
|
207
|
+
.command("query-track")
|
|
208
|
+
.description("Pause or resume weekly checks on a query. Tracking uses a plan slot; pausing frees it")
|
|
209
|
+
.argument("<queryId>", "Query id (from the queries command)")
|
|
210
|
+
.argument("<state>", "on | off")
|
|
211
|
+
.action((queryId, state) =>
|
|
212
|
+
api.setQueryTracking(queryId, state === "on").then(out).catch(fail)
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
program
|
|
216
|
+
.command("query-edit")
|
|
217
|
+
.description("Rewrite a tracked query's text")
|
|
218
|
+
.argument("<queryId>", "Query id (from the queries command)")
|
|
219
|
+
.argument("<queryText>", "New query text")
|
|
220
|
+
.action((queryId, queryText) =>
|
|
221
|
+
api.updateQuery(queryId, queryText).then(out).catch(fail)
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
program
|
|
225
|
+
.command("competitors")
|
|
226
|
+
.description("Tracked competitors used in share-of-voice and visibility comparisons")
|
|
227
|
+
.action(() => api.competitors().then(out).catch(fail));
|
|
228
|
+
|
|
229
|
+
program
|
|
230
|
+
.command("competitor-add")
|
|
231
|
+
.description("Track a competitor (max 10)")
|
|
232
|
+
.argument("<name>", "Competitor name")
|
|
233
|
+
.argument("<websiteUrl>", "Competitor website URL")
|
|
234
|
+
.action((name, websiteUrl) =>
|
|
235
|
+
api.addCompetitor(name, websiteUrl).then(out).catch(fail)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
program
|
|
239
|
+
.command("competitor-remove")
|
|
240
|
+
.description("Stop tracking a competitor")
|
|
241
|
+
.argument("<competitorId>", "Competitor id (from the competitors command)")
|
|
242
|
+
.action((competitorId) =>
|
|
243
|
+
api.removeCompetitor(competitorId).then(out).catch(fail)
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
program
|
|
247
|
+
.command("topics")
|
|
248
|
+
.description("The pillar list (topic clusters): queries, ideas and planned articles group under these")
|
|
249
|
+
.action(() => api.topics().then(out).catch(fail));
|
|
250
|
+
|
|
251
|
+
program
|
|
252
|
+
.command("topics-set")
|
|
253
|
+
.description("Replace the full pillar list (create/dedupe/safe-delete reconcile)")
|
|
254
|
+
.argument("<topics...>", "Topic names, space-separated (quote multi-word topics)")
|
|
255
|
+
.action((topics) => api.setTopics(topics).then(out).catch(fail));
|
|
256
|
+
|
|
257
|
+
program
|
|
258
|
+
.command("delete-planned")
|
|
259
|
+
.description("Remove a planned title from the calendar; the title returns to Content Ideas")
|
|
260
|
+
.argument("<contentId>", "Planned contentPage id")
|
|
261
|
+
.action((contentId) => api.deletePlanned(contentId).then(out).catch(fail));
|
|
262
|
+
|
|
131
263
|
program
|
|
132
264
|
.command("capacity")
|
|
133
265
|
.description("Remaining plan slots on the content calendar")
|
|
@@ -287,6 +419,11 @@ export function runCli(argv) {
|
|
|
287
419
|
api.generateContent(contentId, !!opts.confirm).then(out).catch(fail)
|
|
288
420
|
);
|
|
289
421
|
|
|
422
|
+
program
|
|
423
|
+
.command("shopify-install-url <shop>")
|
|
424
|
+
.description("Mint a Shopify app install link for your store (open it in a browser; the destination is added to the workspace automatically)")
|
|
425
|
+
.action((shop) => api.shopifyInstallUrl(shop).then(out).catch(fail));
|
|
426
|
+
|
|
290
427
|
program
|
|
291
428
|
.command("settings")
|
|
292
429
|
.description("Show the article policy: per-article defaults + flexible scheduling")
|
|
@@ -592,6 +729,21 @@ export function runCli(argv) {
|
|
|
592
729
|
.description("Brand profile, products, and buyer profiles (ICPs) in one read")
|
|
593
730
|
.action(() => api.brand().then(out).catch(fail));
|
|
594
731
|
|
|
732
|
+
program
|
|
733
|
+
.command("brand-set")
|
|
734
|
+
.description("Update brand identity: name, industry, product description, authors, style reference URLs")
|
|
735
|
+
.option("--name <name>", "Company/brand name")
|
|
736
|
+
.option("--industry <industry>", "Industry label")
|
|
737
|
+
.option("--description <text>", "Product description")
|
|
738
|
+
.option("--json <json>", "Full patch as JSON (or @file.json): {name?, industry?, productDescription?, authors?, styleReferenceUrls?}; authors/styleReferenceUrls replace the stored list")
|
|
739
|
+
.action((opts) => {
|
|
740
|
+
const patch = opts.json ? parseJson(opts.json, "brand-set") : {};
|
|
741
|
+
if (opts.name) patch.name = opts.name;
|
|
742
|
+
if (opts.industry) patch.industry = opts.industry;
|
|
743
|
+
if (opts.description) patch.productDescription = opts.description;
|
|
744
|
+
api.brandIdentitySet(patch).then(out).catch(fail);
|
|
745
|
+
});
|
|
746
|
+
|
|
595
747
|
program
|
|
596
748
|
.command("brand-profile-set")
|
|
597
749
|
.description("Patch brand voice/style fields. --json '{\"tone\":\"...\"}' or --json @file.json")
|
package/src/client.mjs
CHANGED
|
@@ -113,6 +113,8 @@ export const api = {
|
|
|
113
113
|
request("POST", "/api/v1/content/publish", { contentId, confirm }),
|
|
114
114
|
generateContent: (contentId, confirm = false) =>
|
|
115
115
|
request("POST", "/api/v1/content/generate", { contentId, confirm }),
|
|
116
|
+
shopifyInstallUrl: (shop) =>
|
|
117
|
+
request("POST", "/api/v1/integrations/shopify/install-url", { shop }),
|
|
116
118
|
internalLinks: (contentId) =>
|
|
117
119
|
request(
|
|
118
120
|
"GET",
|
|
@@ -127,7 +129,6 @@ export const api = {
|
|
|
127
129
|
detectSiteLinks: (source, url) =>
|
|
128
130
|
request("POST", "/api/v1/links/detect", { source, url }),
|
|
129
131
|
addSitePages: (urls) => request("POST", "/api/v1/links/pages", { urls }),
|
|
130
|
-
leads: (limit = 100) => request("GET", `/api/v1/leads?limit=${limit}`),
|
|
131
132
|
repurposeQueue: () => request("GET", "/api/v1/repurpose"),
|
|
132
133
|
repurposeDrafts: (contentId) =>
|
|
133
134
|
request(
|
|
@@ -143,9 +144,39 @@ export const api = {
|
|
|
143
144
|
repurposePush: (opts) => request("POST", "/api/v1/repurpose/push", opts),
|
|
144
145
|
trackedQueries: () => request("GET", "/api/v1/queries"),
|
|
145
146
|
addQuery: (queryText) => request("POST", "/api/v1/queries", { queryText }),
|
|
147
|
+
removeQuery: (queryId) =>
|
|
148
|
+
request("POST", "/api/v1/queries/remove", { queryId }),
|
|
149
|
+
setQueryTracking: (queryId, tracked) =>
|
|
150
|
+
request("POST", "/api/v1/queries/set-tracking", { queryId, tracked }),
|
|
151
|
+
updateQuery: (queryId, queryText) =>
|
|
152
|
+
request("POST", "/api/v1/queries/update", { queryId, queryText }),
|
|
153
|
+
competitors: () => request("GET", "/api/v1/competitors"),
|
|
154
|
+
addCompetitor: (name, websiteUrl) =>
|
|
155
|
+
request("POST", "/api/v1/competitors", { name, websiteUrl }),
|
|
156
|
+
removeCompetitor: (competitorId) =>
|
|
157
|
+
request("POST", "/api/v1/competitors/remove", { competitorId }),
|
|
158
|
+
topics: () => request("GET", "/api/v1/topics"),
|
|
159
|
+
setTopics: (topics) => request("POST", "/api/v1/topics", { topics }),
|
|
160
|
+
deletePlanned: (contentId) =>
|
|
161
|
+
request("POST", "/api/v1/content/delete-planned", { contentId }),
|
|
146
162
|
crawlerAccess: () => request("GET", "/api/v1/crawler-access"),
|
|
147
163
|
trafficOverview: (days = 30) =>
|
|
148
164
|
request("GET", `/api/v1/analytics/traffic?days=${days}`),
|
|
165
|
+
analyticsSources: () => request("GET", "/api/v1/analytics/sources"),
|
|
166
|
+
setAnalyticsSource: (dataType, source) =>
|
|
167
|
+
request("POST", "/api/v1/analytics/sources", { dataType, source }),
|
|
168
|
+
activateScreen: (screen) =>
|
|
169
|
+
request("POST", "/api/v1/analytics/activate", { screen }),
|
|
170
|
+
cloudflareConnectUrl: () =>
|
|
171
|
+
request("POST", "/api/v1/integrations/cloudflare/connect-url"),
|
|
172
|
+
cloudflareZones: () =>
|
|
173
|
+
request("GET", "/api/v1/integrations/cloudflare/zones"),
|
|
174
|
+
cloudflareSelectZone: (zoneId, zoneName) =>
|
|
175
|
+
request("POST", "/api/v1/integrations/cloudflare/zone", { zoneId, zoneName }),
|
|
176
|
+
framerInstallEmbed: (opts = {}) =>
|
|
177
|
+
request("POST", "/api/v1/integrations/framer/install-embed", opts),
|
|
178
|
+
webflowInstallCustomCode: (opts = {}) =>
|
|
179
|
+
request("POST", "/api/v1/integrations/webflow/custom-code", opts),
|
|
149
180
|
team: () => request("GET", "/api/v1/team"),
|
|
150
181
|
teamInvite: (opts) => request("POST", "/api/v1/team/invite", opts),
|
|
151
182
|
teamRevoke: (invitationId) =>
|
|
@@ -202,6 +233,7 @@ export const api = {
|
|
|
202
233
|
request("POST", "/api/v1/social/draft-reply", { threadId, mentionMode }),
|
|
203
234
|
support: (opts) => request("POST", "/api/v1/support", opts),
|
|
204
235
|
brand: () => request("GET", "/api/v1/brand"),
|
|
236
|
+
brandIdentitySet: (opts) => request("POST", "/api/v1/brand", opts),
|
|
205
237
|
brandProfileSet: (opts) => request("POST", "/api/v1/brand/profile", opts),
|
|
206
238
|
brandProductWrite: (opts) => request("POST", "/api/v1/brand/product", opts),
|
|
207
239
|
brandIcpWrite: (opts) => request("POST", "/api/v1/brand/icp", opts),
|
package/src/login.mjs
CHANGED
|
@@ -6,13 +6,20 @@ import {
|
|
|
6
6
|
deleteStoredConfig,
|
|
7
7
|
} from "./client.mjs";
|
|
8
8
|
|
|
9
|
+
// Owner self-connection gets every grantable scope (server grants the full
|
|
10
|
+
// set on approval regardless; "admin" keys are minted in Settings only).
|
|
9
11
|
const DEFAULT_SCOPES = [
|
|
10
12
|
"read:citations",
|
|
11
13
|
"read:content",
|
|
12
|
-
"read:leads",
|
|
13
14
|
"read:analytics",
|
|
14
15
|
"write:content",
|
|
16
|
+
"write:queries",
|
|
17
|
+
"write:backlinks",
|
|
18
|
+
"write:social",
|
|
15
19
|
"write:publish",
|
|
20
|
+
"manage:integrations",
|
|
21
|
+
"manage:org",
|
|
22
|
+
"trigger:agents",
|
|
16
23
|
];
|
|
17
24
|
|
|
18
25
|
function openBrowser(url) {
|
|
@@ -31,9 +38,9 @@ function openBrowser(url) {
|
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
export async function login(
|
|
41
|
+
export async function login() {
|
|
35
42
|
const { baseUrl, appUrl } = getBaseUrls();
|
|
36
|
-
const requested =
|
|
43
|
+
const requested = DEFAULT_SCOPES;
|
|
37
44
|
|
|
38
45
|
const startRes = await fetch(`${baseUrl}/api/v1/auth/device/start`, {
|
|
39
46
|
method: "POST",
|
package/src/mcp.mjs
CHANGED
|
@@ -29,11 +29,11 @@ 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.8.0" });
|
|
33
33
|
|
|
34
34
|
server.tool(
|
|
35
35
|
"get_overview_funnel",
|
|
36
|
-
"AI pipeline overview for the last 30 days: AI crawler hits
|
|
36
|
+
"AI pipeline overview for the last 30 days: AI crawler hits and visits referred by AI engines.",
|
|
37
37
|
{},
|
|
38
38
|
run(() => api.overviewFunnel())
|
|
39
39
|
);
|
|
@@ -90,13 +90,6 @@ export async function startMcpServer() {
|
|
|
90
90
|
run(({ days }) => api.shareOfVoice(days ?? 30))
|
|
91
91
|
);
|
|
92
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
93
|
server.tool(
|
|
101
94
|
"list_tracked_queries",
|
|
102
95
|
"The workspace's tracked queries — the questions RankControl checks weekly across the 6 AI engines for brand citations — with their status and metadata.",
|
|
@@ -106,11 +99,93 @@ export async function startMcpServer() {
|
|
|
106
99
|
|
|
107
100
|
server.tool(
|
|
108
101
|
"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
|
|
102
|
+
"Daily AI-crawler reachability probe result — a DIAGNOSTIC, not a traffic metric: whether the site's edge (e.g. Cloudflare Bot Fight Mode) or robots.txt blocks GPTBot/ClaudeBot. 'shielded' means browsers and bots are both challenged (site-wide shield, ambiguous for real, signed crawlers).",
|
|
110
103
|
{},
|
|
111
104
|
run(() => api.crawlerAccess())
|
|
112
105
|
);
|
|
113
106
|
|
|
107
|
+
server.tool(
|
|
108
|
+
"get_analytics_sources",
|
|
109
|
+
"Per-workspace analytics source setup: the selected writer for human traffic and AI-crawler data, which sources the setup qualifies for, and install state (Cloudflare zone/sync, WordPress plugin version, embed liveness). Both Analytics and Reports screens ship disabled until activated.",
|
|
110
|
+
{},
|
|
111
|
+
run(() => api.analyticsSources())
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
server.tool(
|
|
115
|
+
"set_analytics_source",
|
|
116
|
+
"Select the active writer for a data type. Validated server-side against what the customer's setup supports. Switching aiCrawlers to cloudflare starts a backfill sync.",
|
|
117
|
+
{
|
|
118
|
+
dataType: z.enum(["humanTraffic", "aiCrawlers"]),
|
|
119
|
+
source: z
|
|
120
|
+
.string()
|
|
121
|
+
.describe("embed, wordpress_plugin, cloudflare or none"),
|
|
122
|
+
},
|
|
123
|
+
run(({ dataType, source }) => api.setAnalyticsSource(dataType, source))
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
server.tool(
|
|
127
|
+
"activate_analytics_screen",
|
|
128
|
+
"Turn on the Analytics or Reports screen for this workspace (they ship disabled; activation is one-time per screen).",
|
|
129
|
+
{ screen: z.enum(["analytics", "reports"]) },
|
|
130
|
+
run(({ screen }) => api.activateScreen(screen))
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
server.tool(
|
|
134
|
+
"get_cloudflare_connect_url",
|
|
135
|
+
"OAuth URL to connect read-only Cloudflare analytics (AI-crawler data source). Open it in a browser; the grant is scoped to Analytics:Read.",
|
|
136
|
+
{},
|
|
137
|
+
run(() => api.cloudflareConnectUrl())
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
server.tool(
|
|
141
|
+
"list_cloudflare_zones",
|
|
142
|
+
"Cloudflare zones visible to the connected grant, for picking which zone to poll.",
|
|
143
|
+
{},
|
|
144
|
+
run(() => api.cloudflareZones())
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
server.tool(
|
|
148
|
+
"select_cloudflare_zone",
|
|
149
|
+
"Pick the Cloudflare zone to poll. Also selects Cloudflare as the AI-crawler source and starts the first sync (backfills up to 7 days).",
|
|
150
|
+
{
|
|
151
|
+
zoneId: z.string(),
|
|
152
|
+
zoneName: z.string().describe("Zone name, e.g. example.com"),
|
|
153
|
+
},
|
|
154
|
+
run(({ zoneId, zoneName }) => api.cloudflareSelectZone(zoneId, zoneName))
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
server.tool(
|
|
158
|
+
"install_framer_embed",
|
|
159
|
+
"Install site-wide visit tracking on the connected Framer project via its custom-code API. Dry run unless confirm is true. Confirming ALSO PUBLISHES the customer's Framer site.",
|
|
160
|
+
{
|
|
161
|
+
integrationId: z.string().optional(),
|
|
162
|
+
confirm: z.boolean().optional().describe("Actually install (default: dry run)"),
|
|
163
|
+
},
|
|
164
|
+
run(({ integrationId, confirm }) =>
|
|
165
|
+
api.framerInstallEmbed({
|
|
166
|
+
...(integrationId ? { integrationId } : {}),
|
|
167
|
+
confirm: confirm === true,
|
|
168
|
+
})
|
|
169
|
+
)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
server.tool(
|
|
173
|
+
"install_webflow_custom_code",
|
|
174
|
+
"Register and apply the tracking loader via Webflow's Custom Code API. Dry run unless confirm is true. Needs a site token with CMS + Custom code scopes (pass apiToken to upgrade the stored one). Goes live on the customer's next Webflow site publish.",
|
|
175
|
+
{
|
|
176
|
+
integrationId: z.string().optional(),
|
|
177
|
+
apiToken: z.string().optional().describe("New site token with CMS + Custom code scopes"),
|
|
178
|
+
confirm: z.boolean().optional().describe("Actually install (default: dry run)"),
|
|
179
|
+
},
|
|
180
|
+
run(({ integrationId, apiToken, confirm }) =>
|
|
181
|
+
api.webflowInstallCustomCode({
|
|
182
|
+
...(integrationId ? { integrationId } : {}),
|
|
183
|
+
...(apiToken ? { apiToken } : {}),
|
|
184
|
+
confirm: confirm === true,
|
|
185
|
+
})
|
|
186
|
+
)
|
|
187
|
+
);
|
|
188
|
+
|
|
114
189
|
server.tool(
|
|
115
190
|
"add_tracked_query",
|
|
116
191
|
"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.",
|
|
@@ -118,6 +193,80 @@ export async function startMcpServer() {
|
|
|
118
193
|
run(({ queryText }) => api.addQuery(queryText))
|
|
119
194
|
);
|
|
120
195
|
|
|
196
|
+
server.tool(
|
|
197
|
+
"remove_tracked_query",
|
|
198
|
+
"Delete a query from the tracking pool. Frees its plan slot if it was tracking.",
|
|
199
|
+
{ queryId: z.string().describe("Query id from get_tracked_queries") },
|
|
200
|
+
run(({ queryId }) => api.removeQuery(queryId))
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
server.tool(
|
|
204
|
+
"set_query_tracking",
|
|
205
|
+
"Pause or resume weekly AI citation checks on a pooled query. Tracking consumes one plan slot; pausing frees it. The query stays in the pool either way.",
|
|
206
|
+
{
|
|
207
|
+
queryId: z.string().describe("Query id from get_tracked_queries"),
|
|
208
|
+
tracked: z.boolean().describe("true to track weekly, false to pause"),
|
|
209
|
+
},
|
|
210
|
+
run(({ queryId, tracked }) => api.setQueryTracking(queryId, tracked))
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
server.tool(
|
|
214
|
+
"update_tracked_query",
|
|
215
|
+
"Rewrite a tracked query's text. Future weekly checks use the new text.",
|
|
216
|
+
{
|
|
217
|
+
queryId: z.string().describe("Query id from get_tracked_queries"),
|
|
218
|
+
queryText: z.string().describe("New query text (min 5 characters)"),
|
|
219
|
+
},
|
|
220
|
+
run(({ queryId, queryText }) => api.updateQuery(queryId, queryText))
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
server.tool(
|
|
224
|
+
"get_competitors",
|
|
225
|
+
"Tracked competitors used in share-of-voice and visibility comparisons.",
|
|
226
|
+
{},
|
|
227
|
+
run(() => api.competitors())
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
server.tool(
|
|
231
|
+
"add_competitor",
|
|
232
|
+
"Track a competitor (max 10). They enter weekly share-of-voice comparisons against the org's own citation rate.",
|
|
233
|
+
{
|
|
234
|
+
name: z.string().describe("Competitor name"),
|
|
235
|
+
websiteUrl: z.string().describe("Competitor website URL"),
|
|
236
|
+
},
|
|
237
|
+
run(({ name, websiteUrl }) => api.addCompetitor(name, websiteUrl))
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
server.tool(
|
|
241
|
+
"remove_competitor",
|
|
242
|
+
"Stop tracking a competitor.",
|
|
243
|
+
{ competitorId: z.string().describe("Competitor id from get_competitors") },
|
|
244
|
+
run(({ competitorId }) => api.removeCompetitor(competitorId))
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
server.tool(
|
|
248
|
+
"get_topics",
|
|
249
|
+
"The org's pillar list (topic clusters). Tracked queries, content ideas and planned articles all group under these.",
|
|
250
|
+
{},
|
|
251
|
+
run(() => api.topics())
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
server.tool(
|
|
255
|
+
"set_topics",
|
|
256
|
+
"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.",
|
|
257
|
+
{
|
|
258
|
+
topics: z.array(z.string()).describe("The complete list of topic names"),
|
|
259
|
+
},
|
|
260
|
+
run(({ topics }) => api.setTopics(topics))
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
server.tool(
|
|
264
|
+
"delete_planned_title",
|
|
265
|
+
"Remove a planned title from the content calendar. The title returns to Content Ideas as a pool query (deduped), so the topic is not lost. Only planned rows qualify; use archive for generated articles.",
|
|
266
|
+
{ contentId: z.string().describe("Planned contentPage id") },
|
|
267
|
+
run(({ contentId }) => api.deletePlanned(contentId))
|
|
268
|
+
);
|
|
269
|
+
|
|
121
270
|
server.tool(
|
|
122
271
|
"get_traffic_overview",
|
|
123
272
|
"Traffic overview from RankControl's own page analytics: total page views, unique visitors, sessions, bounce rate, and average time on page.",
|
|
@@ -272,6 +421,15 @@ export async function startMcpServer() {
|
|
|
272
421
|
run(({ contentId, confirm }) => api.generateContent(contentId, confirm === true))
|
|
273
422
|
);
|
|
274
423
|
|
|
424
|
+
server.tool(
|
|
425
|
+
"shopify_install_url",
|
|
426
|
+
"Mint an install link for the RankControl Shopify app. A human opens it in a browser and approves the install in Shopify; the Shopify destination is then added to the workspace automatically. The link expires in 10 minutes and minting it has no side effect on its own.",
|
|
427
|
+
{
|
|
428
|
+
shop: z.string().describe("The store's .myshopify.com domain"),
|
|
429
|
+
},
|
|
430
|
+
run(({ shop }) => api.shopifyInstallUrl(shop))
|
|
431
|
+
);
|
|
432
|
+
|
|
275
433
|
server.tool(
|
|
276
434
|
"get_article_settings",
|
|
277
435
|
"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.",
|
|
@@ -620,6 +778,30 @@ export async function startMcpServer() {
|
|
|
620
778
|
run((args) => api.brandProfileSet(args))
|
|
621
779
|
);
|
|
622
780
|
|
|
781
|
+
server.tool(
|
|
782
|
+
"update_brand_identity",
|
|
783
|
+
"Update workspace identity: name, industry, productDescription, authors (EEAT bylines used in article JSON-LD), styleReferenceUrls (articles whose writing style the writer imitates). authors and styleReferenceUrls REPLACE the stored list — call get_brand first and send the full updated list. websiteUrl is immutable and cannot be changed.",
|
|
784
|
+
{
|
|
785
|
+
name: z.string().optional(),
|
|
786
|
+
industry: z.string().optional(),
|
|
787
|
+
productDescription: z.string().optional(),
|
|
788
|
+
authors: z
|
|
789
|
+
.array(
|
|
790
|
+
z.object({
|
|
791
|
+
name: z.string(),
|
|
792
|
+
bio: z.string().optional(),
|
|
793
|
+
linkedinUrl: z
|
|
794
|
+
.string()
|
|
795
|
+
.optional()
|
|
796
|
+
.describe("Person sameAs URL for article JSON-LD"),
|
|
797
|
+
})
|
|
798
|
+
)
|
|
799
|
+
.optional(),
|
|
800
|
+
styleReferenceUrls: z.array(z.string()).optional(),
|
|
801
|
+
},
|
|
802
|
+
run((args) => api.brandIdentitySet(args))
|
|
803
|
+
);
|
|
804
|
+
|
|
623
805
|
server.tool(
|
|
624
806
|
"write_brand_product",
|
|
625
807
|
"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.",
|