rankcontrol 0.1.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 +21 -0
- package/README.md +71 -0
- package/bin/rankcontrol.mjs +4 -0
- package/package.json +34 -0
- package/src/cli.mjs +118 -0
- package/src/client.mjs +58 -0
- package/src/mcp.mjs +123 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Infowick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# rankcontrol
|
|
2
|
+
|
|
3
|
+
RankControl from the terminal or any AI agent. One package, two faces:
|
|
4
|
+
|
|
5
|
+
- **CLI** — `npx rankcontrol <command>` (or the short alias `rctrl` once installed)
|
|
6
|
+
- **MCP server** — `npx rankcontrol mcp` (stdio, for Claude Code, Claude Desktop, Cursor, Codex)
|
|
7
|
+
|
|
8
|
+
Every command calls the same authed, rate-limited, org-scoped API the RankControl dashboard uses. Quotas and approval gates apply server-side no matter which client calls.
|
|
9
|
+
|
|
10
|
+
## Auth
|
|
11
|
+
|
|
12
|
+
Create an API key in **RankControl → Settings → API**, then:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
export RANKCONTROL_API_KEY=rctrl_pk_...
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Scopes are set per key. Read-only keys work for all `get_*`/`list_*` tools.
|
|
19
|
+
|
|
20
|
+
## CLI
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx rankcontrol funnel # AI crawls / AI visits / AI leads (30d)
|
|
24
|
+
npx rankcontrol visibility --days 60 # daily AI visibility score trend
|
|
25
|
+
npx rankcontrol citations --model chatgpt
|
|
26
|
+
npx rankcontrol capacity # remaining content-plan slots
|
|
27
|
+
npx rankcontrol plan-content --topics "bus travel" --max-difficulty 40
|
|
28
|
+
npx rankcontrol commit-titles --file titles.json
|
|
29
|
+
npx rankcontrol publish <contentId> # dry run
|
|
30
|
+
npx rankcontrol publish <contentId> --confirm # actually publish
|
|
31
|
+
npx rankcontrol jobs # async job status (agent runs)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## MCP
|
|
35
|
+
|
|
36
|
+
Claude Code:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
claude mcp add rankcontrol --env RANKCONTROL_API_KEY=rctrl_pk_... -- npx rankcontrol mcp
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or in `.mcp.json` / Cursor `mcp.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"rankcontrol": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["rankcontrol", "mcp"],
|
|
50
|
+
"env": { "RANKCONTROL_API_KEY": "rctrl_pk_..." }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Tools
|
|
57
|
+
|
|
58
|
+
| Tool | What it does |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `get_overview_funnel` | AI crawls / AI visits / AI leads, last 30 days |
|
|
61
|
+
| `get_visibility_trend` | Daily visibility score from stored weekly citation checks |
|
|
62
|
+
| `list_citations` | Recent citation checks per AI model |
|
|
63
|
+
| `get_visibility_score` | Current overall AI visibility score |
|
|
64
|
+
| `get_planning_capacity` | Remaining calendar slots (check before planning) |
|
|
65
|
+
| `plan_content` | Generate candidate titles (capacity-gated, nothing scheduled) |
|
|
66
|
+
| `commit_planned_titles` | Approve reviewed titles onto the calendar |
|
|
67
|
+
| `publish_content` | Dry run by default; `confirm: true` publishes |
|
|
68
|
+
| `list_content` | Content pages with status |
|
|
69
|
+
| `list_jobs` | Async job status (agent runs) |
|
|
70
|
+
|
|
71
|
+
The plan→commit split is intentional: `plan_content` proposes, a human (or a supervising agent) reviews, `commit_planned_titles` approves. `publish_content` is dry-run-first for the same reason.
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rankcontrol",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "RankControl CLI + MCP server: drive your SEO/AI-visibility workspace from the terminal or any AI agent",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://rctrl.com",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"rankcontrol": "bin/rankcontrol.mjs",
|
|
10
|
+
"rctrl": "bin/rankcontrol.mjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"zod": "^3.24.0"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"seo",
|
|
28
|
+
"aeo",
|
|
29
|
+
"ai-visibility",
|
|
30
|
+
"mcp",
|
|
31
|
+
"model-context-protocol",
|
|
32
|
+
"rankcontrol"
|
|
33
|
+
]
|
|
34
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { api } from "./client.mjs";
|
|
4
|
+
|
|
5
|
+
const out = (data) => console.log(JSON.stringify(data, null, 2));
|
|
6
|
+
|
|
7
|
+
const fail = (err) => {
|
|
8
|
+
console.error(`Error: ${err?.message ?? String(err)}`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const list = (val) => val.split(",").map((s) => s.trim()).filter(Boolean);
|
|
13
|
+
|
|
14
|
+
export function runCli(argv) {
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name("rankcontrol")
|
|
18
|
+
.description(
|
|
19
|
+
"RankControl from the terminal. Auth: export RANKCONTROL_API_KEY=rctrl_pk_... (Settings → API)"
|
|
20
|
+
)
|
|
21
|
+
.version("0.1.0");
|
|
22
|
+
|
|
23
|
+
program
|
|
24
|
+
.command("mcp")
|
|
25
|
+
.description("Start the MCP server on stdio (for Claude, Cursor, and other agents)")
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const { startMcpServer } = await import("./mcp.mjs");
|
|
28
|
+
await startMcpServer().catch(fail);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
program
|
|
32
|
+
.command("funnel")
|
|
33
|
+
.description("AI pipeline last 30 days: crawls, AI visits, AI leads")
|
|
34
|
+
.action(() => api.overviewFunnel().then(out).catch(fail));
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command("visibility")
|
|
38
|
+
.description("Daily AI visibility score trend from the stored weekly citation checks")
|
|
39
|
+
.option("--days <n>", "30, 60 or 90", "30")
|
|
40
|
+
.action((opts) => api.visibilityTrend(Number(opts.days)).then(out).catch(fail));
|
|
41
|
+
|
|
42
|
+
program
|
|
43
|
+
.command("citations")
|
|
44
|
+
.description("Recent AI citation checks")
|
|
45
|
+
.option("--model <model>", "Filter by AI model (chatgpt, perplexity, ...)")
|
|
46
|
+
.option("--limit <n>", "Max rows", "50")
|
|
47
|
+
.action((opts) =>
|
|
48
|
+
api
|
|
49
|
+
.citations({ model: opts.model, limit: Number(opts.limit) })
|
|
50
|
+
.then(out)
|
|
51
|
+
.catch(fail)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command("capacity")
|
|
56
|
+
.description("Remaining plan slots on the content calendar")
|
|
57
|
+
.action(() => api.planningCapacity().then(out).catch(fail));
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command("jobs")
|
|
61
|
+
.description("Recent agent runs (async job status)")
|
|
62
|
+
.option("--limit <n>", "Max rows", "20")
|
|
63
|
+
.action((opts) => api.jobs(Number(opts.limit)).then(out).catch(fail));
|
|
64
|
+
|
|
65
|
+
program
|
|
66
|
+
.command("content")
|
|
67
|
+
.description("List content pages with status")
|
|
68
|
+
.action(() => api.content().then(out).catch(fail));
|
|
69
|
+
|
|
70
|
+
program
|
|
71
|
+
.command("plan-content")
|
|
72
|
+
.description("Generate candidate titles (nothing is scheduled until commit-titles)")
|
|
73
|
+
.option("--topics <a,b,c>", "Focus topics")
|
|
74
|
+
.option("--content-types <a,b>", "Content types")
|
|
75
|
+
.option("--max-difficulty <n>", "Keyword-difficulty ceiling 0-100")
|
|
76
|
+
.option("--intents <a,b>", "informational,commercial,transactional")
|
|
77
|
+
.option("--citation-gaps", "Target queries where AI cites competitors but not us")
|
|
78
|
+
.option("--ranking-gaps", "Target keywords ranking page 2-3 or slipping")
|
|
79
|
+
.action((opts) =>
|
|
80
|
+
api
|
|
81
|
+
.planContent({
|
|
82
|
+
topics: opts.topics ? list(opts.topics) : undefined,
|
|
83
|
+
contentTypes: opts.contentTypes ? list(opts.contentTypes) : undefined,
|
|
84
|
+
maxDifficulty: opts.maxDifficulty ? Number(opts.maxDifficulty) : undefined,
|
|
85
|
+
intents: opts.intents ? list(opts.intents) : undefined,
|
|
86
|
+
useCitationGaps: opts.citationGaps || undefined,
|
|
87
|
+
useRankingGaps: opts.rankingGaps || undefined,
|
|
88
|
+
})
|
|
89
|
+
.then(out)
|
|
90
|
+
.catch(fail)
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
program
|
|
94
|
+
.command("commit-titles")
|
|
95
|
+
.description("Approve reviewed titles onto the calendar. Reads {titles:[...],discarded?:[...]} JSON")
|
|
96
|
+
.option("--file <path>", "JSON file (defaults to stdin)")
|
|
97
|
+
.action(async (opts) => {
|
|
98
|
+
try {
|
|
99
|
+
const raw = opts.file
|
|
100
|
+
? readFileSync(opts.file, "utf8")
|
|
101
|
+
: readFileSync(0, "utf8");
|
|
102
|
+
const body = JSON.parse(raw);
|
|
103
|
+
out(await api.commitPlannedTitles(body));
|
|
104
|
+
} catch (err) {
|
|
105
|
+
fail(err);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
program
|
|
110
|
+
.command("publish <contentId>")
|
|
111
|
+
.description("Publish an article to the connected CMS (dry run unless --confirm)")
|
|
112
|
+
.option("--confirm", "Actually publish (default is a dry run)")
|
|
113
|
+
.action((contentId, opts) =>
|
|
114
|
+
api.publishContent(contentId, !!opts.confirm).then(out).catch(fail)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
program.parseAsync(argv);
|
|
118
|
+
}
|
package/src/client.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const DEFAULT_BASE = "https://api.rctrl.com";
|
|
2
|
+
|
|
3
|
+
export function getConfig() {
|
|
4
|
+
const apiKey = process.env.RANKCONTROL_API_KEY;
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
throw new Error(
|
|
7
|
+
"RANKCONTROL_API_KEY is not set. Create a key in RankControl → Settings → API, then export RANKCONTROL_API_KEY=rctrl_pk_..."
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
apiKey,
|
|
12
|
+
baseUrl: (process.env.RANKCONTROL_API_URL || DEFAULT_BASE).replace(/\/$/, ""),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function request(method, path, body) {
|
|
17
|
+
const { apiKey, baseUrl } = getConfig();
|
|
18
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bearer ${apiKey}`,
|
|
22
|
+
...(body !== undefined ? { "Content-Type": "application/json" } : {}),
|
|
23
|
+
},
|
|
24
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
25
|
+
});
|
|
26
|
+
let json;
|
|
27
|
+
try {
|
|
28
|
+
json = await res.json();
|
|
29
|
+
} catch {
|
|
30
|
+
throw new Error(`RankControl API returned a non-JSON response (HTTP ${res.status})`);
|
|
31
|
+
}
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(json?.error || `RankControl API error (HTTP ${res.status})`);
|
|
34
|
+
}
|
|
35
|
+
return json.data ?? json;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const api = {
|
|
39
|
+
overviewFunnel: () => request("GET", "/api/v1/overview/funnel"),
|
|
40
|
+
visibilityTrend: (days = 30) =>
|
|
41
|
+
request("GET", `/api/v1/visibility/trend?days=${days}`),
|
|
42
|
+
citations: (params = {}) => {
|
|
43
|
+
const q = new URLSearchParams();
|
|
44
|
+
if (params.model) q.set("model", params.model);
|
|
45
|
+
if (params.limit) q.set("limit", String(params.limit));
|
|
46
|
+
const qs = q.toString();
|
|
47
|
+
return request("GET", `/api/v1/citations${qs ? `?${qs}` : ""}`);
|
|
48
|
+
},
|
|
49
|
+
visibilityScore: () => request("GET", "/api/v1/visibility/score"),
|
|
50
|
+
planningCapacity: () => request("GET", "/api/v1/content/planning-capacity"),
|
|
51
|
+
jobs: (limit = 20) => request("GET", `/api/v1/jobs?limit=${limit}`),
|
|
52
|
+
content: () => request("GET", "/api/v1/content"),
|
|
53
|
+
planContent: (opts) => request("POST", "/api/v1/content/plan", opts),
|
|
54
|
+
commitPlannedTitles: (opts) =>
|
|
55
|
+
request("POST", "/api/v1/content/plan/commit", opts),
|
|
56
|
+
publishContent: (contentId, confirm = false) =>
|
|
57
|
+
request("POST", "/api/v1/content/publish", { contentId, confirm }),
|
|
58
|
+
};
|
package/src/mcp.mjs
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { api } from "./client.mjs";
|
|
5
|
+
|
|
6
|
+
const asText = (data) => ({
|
|
7
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const asError = (err) => ({
|
|
11
|
+
content: [{ type: "text", text: `Error: ${err?.message ?? String(err)}` }],
|
|
12
|
+
isError: true,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const run = (fn) => async (args) => {
|
|
16
|
+
try {
|
|
17
|
+
return asText(await fn(args));
|
|
18
|
+
} catch (err) {
|
|
19
|
+
return asError(err);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const plannedTitle = z.object({
|
|
24
|
+
title: z.string(),
|
|
25
|
+
targetKeyword: z.string().optional(),
|
|
26
|
+
searchVolume: z.number().optional(),
|
|
27
|
+
keywordDifficulty: z.number().optional(),
|
|
28
|
+
searchIntent: z.string().optional(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export async function startMcpServer() {
|
|
32
|
+
const server = new McpServer({ name: "rankcontrol", version: "0.1.0" });
|
|
33
|
+
|
|
34
|
+
server.tool(
|
|
35
|
+
"get_overview_funnel",
|
|
36
|
+
"AI pipeline overview for the last 30 days: AI crawler hits, visits referred by AI engines, and leads that came from AI search.",
|
|
37
|
+
{},
|
|
38
|
+
run(() => api.overviewFunnel())
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
server.tool(
|
|
42
|
+
"get_visibility_trend",
|
|
43
|
+
"Daily AI visibility score series (share of weekly citation checks where the brand was cited across the 6 tracked AI engines). Data comes from RankControl's stored weekly checks.",
|
|
44
|
+
{ days: z.enum(["30", "60", "90"]).optional().describe("Window size in days, default 30") },
|
|
45
|
+
run(({ days }) => api.visibilityTrend(days ? Number(days) : 30))
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
server.tool(
|
|
49
|
+
"list_citations",
|
|
50
|
+
"Recent AI citation checks: which tracked queries were checked on which AI model and whether the brand was cited.",
|
|
51
|
+
{
|
|
52
|
+
model: z.string().optional().describe("Filter by AI model, e.g. chatgpt, perplexity"),
|
|
53
|
+
limit: z.number().max(200).optional(),
|
|
54
|
+
},
|
|
55
|
+
run((args) => api.citations(args))
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.tool(
|
|
59
|
+
"get_visibility_score",
|
|
60
|
+
"Current overall AI visibility score for the workspace.",
|
|
61
|
+
{},
|
|
62
|
+
run(() => api.visibilityScore())
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
server.tool(
|
|
66
|
+
"get_planning_capacity",
|
|
67
|
+
"How many article titles can still be planned onto the content calendar within the scheduling horizon. Check before plan_content.",
|
|
68
|
+
{},
|
|
69
|
+
run(() => api.planningCapacity())
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
server.tool(
|
|
73
|
+
"list_jobs",
|
|
74
|
+
"Recent agent runs (job status feed). Async actions like content planning show up here with their outputs.",
|
|
75
|
+
{ limit: z.number().max(100).optional() },
|
|
76
|
+
run(({ limit }) => api.jobs(limit ?? 20))
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
server.tool(
|
|
80
|
+
"list_content",
|
|
81
|
+
"List content pages in the workspace with their status (planned, draft, published).",
|
|
82
|
+
{},
|
|
83
|
+
run(() => api.content())
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
server.tool(
|
|
87
|
+
"plan_content",
|
|
88
|
+
"Generate candidate article titles (capacity-gated). This DOES NOT put anything on the calendar: review the returned titles, then call commit_planned_titles with the keepers. Costs an LLM call; capped at 5/min.",
|
|
89
|
+
{
|
|
90
|
+
topics: z.array(z.string()).optional().describe("Brand topics to focus on; omit for auto"),
|
|
91
|
+
contentTypes: z.array(z.string()).optional(),
|
|
92
|
+
maxDifficulty: z.number().min(0).max(100).optional().describe("Keyword-difficulty ceiling"),
|
|
93
|
+
intents: z.array(z.string()).optional().describe("informational / commercial / transactional"),
|
|
94
|
+
useCitationGaps: z.boolean().optional().describe("Target queries where AI cites competitors but not us"),
|
|
95
|
+
useRankingGaps: z.boolean().optional().describe("Target keywords ranking page 2-3 or slipping"),
|
|
96
|
+
},
|
|
97
|
+
run((args) => api.planContent(args))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
server.tool(
|
|
101
|
+
"commit_planned_titles",
|
|
102
|
+
"Approve reviewed titles onto the content calendar (the approval half of the plan→commit gate). Pass rejected candidates in `discarded` so their keywords recycle into the idea pool.",
|
|
103
|
+
{
|
|
104
|
+
titles: z.array(plannedTitle).describe("Titles to schedule"),
|
|
105
|
+
discarded: z.array(plannedTitle).optional().describe("Rejected candidates"),
|
|
106
|
+
},
|
|
107
|
+
run((args) => api.commitPlannedTitles(args))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
server.tool(
|
|
111
|
+
"publish_content",
|
|
112
|
+
"Publish an article to the customer's connected CMS. Defaults to a DRY RUN describing what would happen; a human should approve before calling again with confirm=true.",
|
|
113
|
+
{
|
|
114
|
+
contentId: z.string().describe("The content page id"),
|
|
115
|
+
confirm: z.boolean().optional().describe("Set true to actually publish (default: dry run)"),
|
|
116
|
+
},
|
|
117
|
+
run(({ contentId, confirm }) => api.publishContent(contentId, confirm === true))
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
await server.connect(new StdioServerTransport());
|
|
121
|
+
// Keep the process alive; the transport owns stdin/stdout from here
|
|
122
|
+
console.error("rankcontrol MCP server running on stdio");
|
|
123
|
+
}
|