@cite42/mcp 0.1.15 → 0.1.17
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 +54 -20
- package/dist/stdio.cjs +322 -40
- package/dist/stdio.mjs +322 -40
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for [Cite42, the AI search visibility, SEO keyword & trends API](https://www.cite42.dev): market research tools for your AI agent.
|
|
4
4
|
|
|
5
|
-
Once
|
|
5
|
+
Once connected, Claude Code, Claude Desktop, Codex CLI, or Cursor can:
|
|
6
6
|
|
|
7
7
|
- **Query AI models** (ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews) and see each model's answer and the sources it cites
|
|
8
8
|
- **Track brand visibility** in AI answers: rankings, head-to-head comparisons, and sentiment
|
|
9
|
+
- **Schedule daily or weekly trackers** for rankings, comparisons, citations, and sentiment, then ask your agent for stored run history
|
|
9
10
|
- **Pull SEO data**: keyword search volume, CPC, competition, and Google Trends
|
|
10
11
|
- **Mine Reddit and YouTube** for audience questions, pain points, and rising content
|
|
11
12
|
- **Run full research workflows**: content gaps, topic demand, AI prompt maps, competitor analysis, and content briefs in a single call
|
|
@@ -14,14 +15,16 @@ Once installed, your MCP client (Claude Code, Claude Desktop, Codex CLI, Cursor,
|
|
|
14
15
|
|
|
15
16
|
## Requirements
|
|
16
17
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
18
|
+
- **A Cite42 account**, free to create at [cite42.dev](https://www.cite42.dev)
|
|
19
|
+
- **Node.js 18 or newer and a Cite42 API key** for Claude Code, Codex CLI, and Cursor
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
Claude Desktop connects to Cite42 through the hosted remote MCP URL and OAuth. Claude Code, Codex CLI, and Cursor run this package on demand with `npx -y @cite42/mcp`.
|
|
23
24
|
|
|
24
|
-
### Step 1: Get your API key
|
|
25
|
+
### Step 1: Get your API key for local clients
|
|
26
|
+
|
|
27
|
+
Skip this step for Claude Desktop; its hosted connector authorizes through sign-in.
|
|
25
28
|
|
|
26
29
|
1. Sign in at [cite42.dev](https://www.cite42.dev)
|
|
27
30
|
2. Open **Dashboard → API Keys**: [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys)
|
|
@@ -29,7 +32,7 @@ There is nothing to install manually. Every client below runs the server on dema
|
|
|
29
32
|
|
|
30
33
|
### Step 2: Add the server to your client
|
|
31
34
|
|
|
32
|
-
Pick **one** of the clients below and replace `cite42_live_your_key_here` with your real key.
|
|
35
|
+
Pick **one** of the clients below. For Claude Code, Codex CLI, and Cursor, replace `cite42_live_your_key_here` with your real key.
|
|
33
36
|
|
|
34
37
|
#### Claude Code
|
|
35
38
|
|
|
@@ -37,15 +40,29 @@ Pick **one** of the clients below and replace `cite42_live_your_key_here` with y
|
|
|
37
40
|
claude mcp add cite42 -e CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
|
|
38
41
|
```
|
|
39
42
|
|
|
43
|
+
#### Claude Desktop
|
|
44
|
+
|
|
45
|
+
1. Open **Settings → Connectors** and click **Add**.
|
|
46
|
+
2. Select **Add custom connector**.
|
|
47
|
+
3. Enter **Cite42** for the name and paste this remote MCP URL:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
https://mcp.cite42.dev
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
4. Click **Connect** and complete the Cite42 sign-in. No API key is required.
|
|
54
|
+
|
|
55
|
+
The same hosted-connector steps apply in Claude.ai.
|
|
56
|
+
|
|
40
57
|
#### Codex CLI
|
|
41
58
|
|
|
42
59
|
```bash
|
|
43
60
|
codex mcp add cite42 --env CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
|
|
44
61
|
```
|
|
45
62
|
|
|
46
|
-
####
|
|
63
|
+
#### Cursor
|
|
47
64
|
|
|
48
|
-
Add this
|
|
65
|
+
Add this under **Settings → MCP**, or save it to `~/.cursor/mcp.json`:
|
|
49
66
|
|
|
50
67
|
```json
|
|
51
68
|
{
|
|
@@ -61,36 +78,34 @@ Add this to `claude_desktop_config.json` (**Settings → Developer → Edit Conf
|
|
|
61
78
|
}
|
|
62
79
|
```
|
|
63
80
|
|
|
64
|
-
#### Cursor / Windsurf / other JSON-config clients
|
|
65
|
-
|
|
66
|
-
Use the same JSON block as Claude Desktop in your client's `mcpServers` config (Cursor: `~/.cursor/mcp.json` or **Settings → MCP**).
|
|
67
|
-
|
|
68
81
|
### Step 3: Restart so the server loads
|
|
69
82
|
|
|
70
|
-
MCP servers are started by the client, so
|
|
83
|
+
Local MCP servers are started by the client, so their config is only picked up on a fresh start. The Claude Desktop connector is ready after sign-in.
|
|
71
84
|
|
|
72
85
|
- **Claude Code / Codex CLI**: exit the current session and open a **new terminal** (or start a new `claude` / `codex` session)
|
|
73
|
-
- **
|
|
86
|
+
- **Cursor**: fully quit and reopen the app
|
|
74
87
|
|
|
75
|
-
You do not need to keep `npx -y @cite42/mcp` running in a separate terminal.
|
|
88
|
+
You do not need to keep `npx -y @cite42/mcp` running in a separate terminal. Local MCP clients start it automatically when the session needs tools.
|
|
76
89
|
|
|
77
90
|
### Step 4: Verify it's running
|
|
78
91
|
|
|
79
92
|
- **Claude Code:** run `claude mcp list` and check that `cite42` shows as ✔ connected. Or type `/mcp` inside a session.
|
|
93
|
+
- **Claude Desktop:** open **Settings → Connectors** and confirm Cite42 is connected.
|
|
80
94
|
- **Codex CLI:** run `codex mcp list`.
|
|
81
|
-
- **
|
|
95
|
+
- **Cursor:** look for `cite42` in the MCP/tools indicator. 26 tools should be listed.
|
|
82
96
|
|
|
83
97
|
If it doesn't appear, see [Troubleshooting](#troubleshooting).
|
|
84
98
|
|
|
85
99
|
## Try it: example prompts
|
|
86
100
|
|
|
87
|
-
After restarting, just ask in plain language. Your agent picks the right Cite42 tool automatically.
|
|
101
|
+
After connecting or restarting, just ask in plain language. Your agent picks the right Cite42 tool automatically.
|
|
88
102
|
|
|
89
103
|
- *"Use Cite42 to check which brands AI models recommend for 'best project management software'."*
|
|
90
104
|
- *"Which sources do AI models cite when asked about CRM tools for startups? Is mycompany.com among them?"*
|
|
91
105
|
- *"Get search volume, CPC, and related keywords for 'ai seo tools'."*
|
|
92
106
|
- *"What are people on Reddit complaining about regarding email marketing tools?"*
|
|
93
107
|
- *"Run a content brief for the topic 'ai website builder'."*
|
|
108
|
+
- *"Create a weekly Cite42 tracker for our rankings, citations, and sentiment. Show me the estimated credits per run before activating it."*
|
|
94
109
|
|
|
95
110
|
### Example: what a session looks like
|
|
96
111
|
|
|
@@ -111,7 +126,15 @@ After restarting, just ask in plain language. Your agent picks the right Cite42
|
|
|
111
126
|
|
|
112
127
|
The exact brands and sources change by query and model. A good result should show the tool used, model/source evidence, and a short summary you can act on.
|
|
113
128
|
|
|
114
|
-
##
|
|
129
|
+
## Tools
|
|
130
|
+
|
|
131
|
+
Cite42 exposes 26 MCP tools: 3 free account tools, 9 primitive data tools, 6 research workflows, and 8 tracker tools.
|
|
132
|
+
|
|
133
|
+
### Account & pricing (free)
|
|
134
|
+
|
|
135
|
+
- `cite42_credits`: check the connected account's remaining Cite42 credit balance.
|
|
136
|
+
- `cite42_pricing`: get current per-tool prices and estimate a planned set of calls.
|
|
137
|
+
- `cite42_usage`: read recent call status and cost together with the remaining balance.
|
|
115
138
|
|
|
116
139
|
### AI search visibility
|
|
117
140
|
|
|
@@ -123,7 +146,7 @@ The exact brands and sources change by query and model. A good result should sho
|
|
|
123
146
|
|
|
124
147
|
### SEO & trend data
|
|
125
148
|
|
|
126
|
-
- `cite42_keywords`: get search volume, CPC, competition,
|
|
149
|
+
- `cite42_keywords`: get seed search volume, CPC, and competition, with optional related keyword ideas.
|
|
127
150
|
- `cite42_trends`: get Google Trends interest over time, related and rising queries, and a trend label.
|
|
128
151
|
- `cite42_reddit_trends`: find Reddit audience questions, pain points, rising threads, and product mentions.
|
|
129
152
|
- `cite42_youtube_trends`: find YouTube rising videos, creator angles, title patterns, and opportunities.
|
|
@@ -137,6 +160,17 @@ The exact brands and sources change by query and model. A good result should sho
|
|
|
137
160
|
- `cite42_find_ai_citation_gaps`: find queries where AI answers cite competitors but not your brand, domain, or URL.
|
|
138
161
|
- `cite42_generate_content_brief`: collect AI answers, citations, keyword demand, and social signals into brief-ready data.
|
|
139
162
|
|
|
163
|
+
### Trackers
|
|
164
|
+
|
|
165
|
+
- `cite42_tracker_create`: create a draft daily or weekly tracker from saved rankings, compare, citations, and sentiment inputs. Creating a draft is free and does not schedule calls.
|
|
166
|
+
- `cite42_tracker_update`: replace a tracker's name, cadence, and saved primitive inputs. Updating configuration is free.
|
|
167
|
+
- `cite42_tracker_activate`: activate or resume recurring runs. You must pass the exact `estimatedCostMicroPerRun` from the latest tracker record as `maxCostMicroPerRun`; activation authorizes billed scheduled runs until the tracker is paused.
|
|
168
|
+
- `cite42_tracker_pause`: stop future scheduled runs while preserving configuration and history. Pausing is free.
|
|
169
|
+
- `cite42_tracker_delete`: permanently delete a draft or paused tracker and its run history. Active trackers must be paused first, and deletion is rejected while a run is queued or running. Financial usage records remain available.
|
|
170
|
+
- `cite42_tracker_run`: queue one manual run from the saved primitive inputs. The underlying primitive calls are billed at their normal rates.
|
|
171
|
+
- `cite42_trackers`: list trackers, schedules, statuses, cost estimates, and last/next run times. Reading the list is free.
|
|
172
|
+
- `cite42_tracker_report`: read bounded stored run history with results, changes, status, sampled time, and costs (latest 8 by default, up to 20). Reading stored reports is free.
|
|
173
|
+
|
|
140
174
|
## Configuration
|
|
141
175
|
|
|
142
176
|
- `CITE42_API_KEY`: required. Your API key from [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys).
|
|
@@ -150,7 +184,7 @@ npx -y @cite42/mcp --api-key=cite42_live_your_key_here --api-base=https://www.ci
|
|
|
150
184
|
|
|
151
185
|
## Billing
|
|
152
186
|
|
|
153
|
-
|
|
187
|
+
Primitive and workflow calls consume Cite42 credits from your account. Tracker management and stored-history reads are free; every scheduled or manual tracker run bills its saved primitive calls at their normal rates. Activating or resuming a tracker requires explicit confirmation of the current maximum credits per run and authorizes recurring billed calls until you pause it. Calls fail with a clear error if the API key is missing or credits run out. See [www.cite42.dev/pricing](https://www.cite42.dev/pricing).
|
|
154
188
|
|
|
155
189
|
## Troubleshooting
|
|
156
190
|
|
package/dist/stdio.cjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
// src/stdio.ts
|
|
5
|
-
var import_mcp = require("@mastra/mcp");
|
|
6
|
-
|
|
7
4
|
// src/client.ts
|
|
8
5
|
var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
|
|
9
6
|
var MICRO_CREDITS_PER_CREDIT = 1e3;
|
|
@@ -40,6 +37,14 @@ function toCite42McpResult(value) {
|
|
|
40
37
|
}
|
|
41
38
|
};
|
|
42
39
|
}
|
|
40
|
+
function toCite42CreditsBalance(balanceMicroCredits) {
|
|
41
|
+
return {
|
|
42
|
+
cite42Credits: {
|
|
43
|
+
remaining: toCredits(balanceMicroCredits),
|
|
44
|
+
message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
43
48
|
function errorMessageForMcp(status, path, body) {
|
|
44
49
|
try {
|
|
45
50
|
const payload = JSON.parse(body);
|
|
@@ -60,6 +65,7 @@ function readFlag(name) {
|
|
|
60
65
|
const prefix = `--${name}=`;
|
|
61
66
|
for (let i = 0; i < argv.length; i++) {
|
|
62
67
|
const arg = argv[i];
|
|
68
|
+
if (!arg) continue;
|
|
63
69
|
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
64
70
|
if (arg === `--${name}` && i + 1 < argv.length) return argv[i + 1];
|
|
65
71
|
}
|
|
@@ -81,9 +87,27 @@ function getBaseUrl() {
|
|
|
81
87
|
return readFlag("api-base") ?? process.env.CITE42_API_BASE ?? DEFAULT_BASE_URL;
|
|
82
88
|
}
|
|
83
89
|
async function aivPost(path, body) {
|
|
90
|
+
return aivWrite("POST", path, body);
|
|
91
|
+
}
|
|
92
|
+
async function aivPatch(path, body) {
|
|
93
|
+
return aivWrite("PATCH", path, body);
|
|
94
|
+
}
|
|
95
|
+
async function aivDelete(path) {
|
|
84
96
|
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
85
97
|
const res = await fetch(url, {
|
|
86
|
-
method: "
|
|
98
|
+
method: "DELETE",
|
|
99
|
+
headers: { Authorization: `Bearer ${getApiKey()}` }
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const body = await res.text();
|
|
103
|
+
throw new Error(errorMessageForMcp(res.status, path, body));
|
|
104
|
+
}
|
|
105
|
+
return toCite42McpResult(await res.json());
|
|
106
|
+
}
|
|
107
|
+
async function aivWrite(method, path, body) {
|
|
108
|
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
109
|
+
const res = await fetch(url, {
|
|
110
|
+
method,
|
|
87
111
|
headers: {
|
|
88
112
|
"Content-Type": "application/json",
|
|
89
113
|
Authorization: `Bearer ${getApiKey()}`
|
|
@@ -96,6 +120,21 @@ async function aivPost(path, body) {
|
|
|
96
120
|
}
|
|
97
121
|
return toCite42McpResult(await res.json());
|
|
98
122
|
}
|
|
123
|
+
async function aivGet(path) {
|
|
124
|
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
125
|
+
const res = await fetch(url, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
headers: { Authorization: `Bearer ${getApiKey()}` }
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
const text = await res.text();
|
|
131
|
+
throw new Error(errorMessageForMcp(res.status, path, text));
|
|
132
|
+
}
|
|
133
|
+
return toCite42McpResult(await res.json());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/server.ts
|
|
137
|
+
var import_mcp = require("@mastra/mcp");
|
|
99
138
|
|
|
100
139
|
// src/tools.ts
|
|
101
140
|
var import_tools = require("@mastra/core/tools");
|
|
@@ -105,9 +144,27 @@ var QUERY_MAX_CHARS = 2e3;
|
|
|
105
144
|
var SHORT_TEXT_MAX_CHARS = 200;
|
|
106
145
|
var TIMEFRAME_MAX_CHARS = 80;
|
|
107
146
|
var MODEL_SELECTION_DESCRIPTION = 'Optional model subset. Omit to query all five AI surfaces (chatgpt, claude, perplexity, gemini, google_ai_overview). Pass ["perplexity"] or ["perplexity","claude"] when the user asks for specific surfaces or wants to reduce cost.';
|
|
108
|
-
var MCP_USAGE_GUIDANCE = "
|
|
109
|
-
var MODEL_PRICING = "Pricing
|
|
110
|
-
var DATA_PRICING = "
|
|
147
|
+
var MCP_USAGE_GUIDANCE = "Covers AI-search, citation, SEO, keyword, trend, and market-research data. If a plan needs more than 20 endpoint calls or may use more than 500 Cite42 credits, estimate the credits and ask the user before continuing. When a result contains cite42Credits, that figure is Cite42 credits, not a model or host balance. Free (0 credits): cite42_credits for the remaining balance, cite42_pricing for exact per-tool prices and cost estimates, cite42_usage for recent calls.";
|
|
148
|
+
var MODEL_PRICING = "Pricing is per selected AI surface; omitting models queries all five. Use the models field when the user names a surface or asks to reduce credit use, and call cite42_pricing for exact current rates.";
|
|
149
|
+
var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
|
|
150
|
+
var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the primitive endpoints that run successfully and the selected model rates.";
|
|
151
|
+
function workflowDescription(description) {
|
|
152
|
+
return `${description} ${MCP_USAGE_GUIDANCE} ${MODEL_PRICING} ${DATA_PRICING} ${WORKFLOW_PRICING}`;
|
|
153
|
+
}
|
|
154
|
+
function readOnly(title, openWorldHint = true) {
|
|
155
|
+
return { annotations: { title, readOnlyHint: true, openWorldHint } };
|
|
156
|
+
}
|
|
157
|
+
function mutation(title, options = {}) {
|
|
158
|
+
return {
|
|
159
|
+
annotations: {
|
|
160
|
+
title,
|
|
161
|
+
readOnlyHint: false,
|
|
162
|
+
destructiveHint: options.destructiveHint ?? false,
|
|
163
|
+
openWorldHint: options.openWorldHint ?? false,
|
|
164
|
+
...options.idempotentHint === void 0 ? {} : { idempotentHint: options.idempotentHint }
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
111
168
|
function queryText(description) {
|
|
112
169
|
return import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
|
|
113
170
|
}
|
|
@@ -119,8 +176,52 @@ function optionalShortText(description) {
|
|
|
119
176
|
}
|
|
120
177
|
var modelsInput = import_zod.z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
|
|
121
178
|
var competitorList = import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
|
|
179
|
+
var aivCredits = (0, import_tools.createTool)({
|
|
180
|
+
id: "cite42_credits",
|
|
181
|
+
mcp: readOnly("Check Cite42 credit balance", false),
|
|
182
|
+
description: "Check the remaining Cite42 credit balance for the connected account. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
|
|
183
|
+
inputSchema: import_zod.z.object({}),
|
|
184
|
+
execute: async () => {
|
|
185
|
+
const usage = await aivGet("/usage");
|
|
186
|
+
const microCredits = usage.balance?.microCredits;
|
|
187
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
188
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
189
|
+
}
|
|
190
|
+
return toCite42CreditsBalance(microCredits);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
var aivPricing = (0, import_tools.createTool)({
|
|
194
|
+
id: "cite42_pricing",
|
|
195
|
+
mcp: readOnly("Get Cite42 price table", false),
|
|
196
|
+
description: 'Get the exact Cite42 price table in credits: per-model rates, flat-rate tools, workflow maximums, and which tools are free. Use it to answer any "how much does X cost" question or to estimate a plan before running it. Free: costs 0 credits. ' + MCP_USAGE_GUIDANCE,
|
|
197
|
+
inputSchema: import_zod.z.object({}),
|
|
198
|
+
execute: async () => aivGet("/pricing")
|
|
199
|
+
});
|
|
200
|
+
var aivUsage = (0, import_tools.createTool)({
|
|
201
|
+
id: "cite42_usage",
|
|
202
|
+
mcp: readOnly("Get recent Cite42 usage", false),
|
|
203
|
+
description: "Get recent Cite42 usage for the connected account: the last calls with their cost and status, plus the remaining credit balance. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
|
|
204
|
+
inputSchema: import_zod.z.object({}),
|
|
205
|
+
execute: async () => {
|
|
206
|
+
const usage = await aivGet("/usage");
|
|
207
|
+
const microCredits = usage.balance?.microCredits;
|
|
208
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
209
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
...toCite42CreditsBalance(microCredits),
|
|
213
|
+
recentCalls: (usage.recent ?? []).slice(0, 20).map((row) => ({
|
|
214
|
+
endpoint: row.endpoint,
|
|
215
|
+
status: row.status,
|
|
216
|
+
credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
|
|
217
|
+
at: row.createdAt
|
|
218
|
+
}))
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
});
|
|
122
222
|
var aivSearch = (0, import_tools.createTool)({
|
|
123
223
|
id: "cite42_search",
|
|
224
|
+
mcp: readOnly("Search AI models"),
|
|
124
225
|
description: `Run a query against AI models (ChatGPT, Claude, Perplexity, Gemini, and Google AI Overviews) and return each model's answer along with the sources it cited. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
125
226
|
inputSchema: import_zod.z.object({
|
|
126
227
|
query: queryText("The concise AI-search/market-research query to run against the selected models."),
|
|
@@ -130,6 +231,7 @@ var aivSearch = (0, import_tools.createTool)({
|
|
|
130
231
|
});
|
|
131
232
|
var aivCitations = (0, import_tools.createTool)({
|
|
132
233
|
id: "cite42_citations",
|
|
234
|
+
mcp: readOnly("Find AI citations"),
|
|
133
235
|
description: `Find which sources AI models cite for a query. Aggregates cited URLs across models with per-model counts; optionally checks whether a specific URL is cited. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
134
236
|
inputSchema: import_zod.z.object({
|
|
135
237
|
query: queryText("The concise query to look up cited sources for."),
|
|
@@ -140,6 +242,7 @@ var aivCitations = (0, import_tools.createTool)({
|
|
|
140
242
|
});
|
|
141
243
|
var aivRankings = (0, import_tools.createTool)({
|
|
142
244
|
id: "cite42_rankings",
|
|
245
|
+
mcp: readOnly("Measure AI brand rankings"),
|
|
143
246
|
description: `Measure how a set of brands/competitors rank in AI answers for a query: mention rate, average position, and a per-model breakdown across ChatGPT, Claude, Perplexity, Gemini, and Google AI Overviews. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
144
247
|
inputSchema: import_zod.z.object({
|
|
145
248
|
query: queryText("The concise query to measure brand/competitor visibility on."),
|
|
@@ -150,6 +253,7 @@ var aivRankings = (0, import_tools.createTool)({
|
|
|
150
253
|
});
|
|
151
254
|
var aivCompare = (0, import_tools.createTool)({
|
|
152
255
|
id: "cite42_compare",
|
|
256
|
+
mcp: readOnly("Compare brand against competitors"),
|
|
153
257
|
description: `Compare one primary brand head-to-head against a competitor set in AI answers: per-brand rankings plus a co-mention graph of which brands the models name together. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
154
258
|
inputSchema: import_zod.z.object({
|
|
155
259
|
query: queryText("The concise query to compare the brand and competitors on."),
|
|
@@ -161,6 +265,7 @@ var aivCompare = (0, import_tools.createTool)({
|
|
|
161
265
|
});
|
|
162
266
|
var aivSentiment = (0, import_tools.createTool)({
|
|
163
267
|
id: "cite42_sentiment",
|
|
268
|
+
mcp: readOnly("Score brand sentiment in AI answers"),
|
|
164
269
|
description: `Score how each AI model talks about a brand for a query: a positive/neutral/negative split per model with the verbatim phrases driving the tone, plus an aggregate across models. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
165
270
|
inputSchema: import_zod.z.object({
|
|
166
271
|
query: queryText("The concise query whose answers are scored for brand sentiment."),
|
|
@@ -171,26 +276,31 @@ var aivSentiment = (0, import_tools.createTool)({
|
|
|
171
276
|
});
|
|
172
277
|
var aivKeywords = (0, import_tools.createTool)({
|
|
173
278
|
id: "cite42_keywords",
|
|
174
|
-
|
|
279
|
+
mcp: readOnly("Get SEO keyword data"),
|
|
280
|
+
description: `Classic keyword data for a seed term. Omit ideas when the user only needs that seed's monthly search volume, CPC, or competition. Set ideas:true only when the user asks for related, alternative, or long-tail suggestions from provider candidates. A flat-priced data tool, not a multi-model AI-search query. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
175
281
|
inputSchema: import_zod.z.object({
|
|
176
282
|
seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
|
|
177
283
|
country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
|
|
178
|
-
ideas: import_zod.z.boolean().optional().describe(
|
|
284
|
+
ideas: import_zod.z.boolean().optional().describe(
|
|
285
|
+
"Omit for volume, CPC, and competition on the seed only. Set true only for related, alternative, or long-tail keyword ideas."
|
|
286
|
+
)
|
|
179
287
|
}),
|
|
180
288
|
execute: async (input) => aivPost("/keywords", input)
|
|
181
289
|
});
|
|
182
290
|
var aivTrends = (0, import_tools.createTool)({
|
|
183
291
|
id: "cite42_trends",
|
|
292
|
+
mcp: readOnly("Get Google Trends data"),
|
|
184
293
|
description: `Google Trends interest-over-time, related and rising queries, and a rising/falling/stable label for a term. A flat-priced data tool, not an AI-model query. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
185
294
|
inputSchema: import_zod.z.object({
|
|
186
295
|
term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
|
|
187
296
|
timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trends window, e.g. "past_12_months", "past_30_days", "past_5_years".'),
|
|
188
|
-
geo: import_zod.z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United
|
|
297
|
+
geo: import_zod.z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United Kingdom"). Defaults to the United States when omitted.')
|
|
189
298
|
}),
|
|
190
299
|
execute: async (input) => aivPost("/trends", input)
|
|
191
300
|
});
|
|
192
301
|
var aivRedditTrends = (0, import_tools.createTool)({
|
|
193
302
|
id: "cite42_reddit_trends",
|
|
303
|
+
mcp: readOnly("Get Reddit trend data"),
|
|
194
304
|
description: `Reddit trend data for audience questions, pain points, rising threads, repeated phrases, mentioned products, and source URLs around a topic. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
195
305
|
inputSchema: import_zod.z.object({
|
|
196
306
|
topic: queryText("Topic, category, product, or audience to research on Reddit."),
|
|
@@ -201,6 +311,7 @@ var aivRedditTrends = (0, import_tools.createTool)({
|
|
|
201
311
|
});
|
|
202
312
|
var aivYoutubeTrends = (0, import_tools.createTool)({
|
|
203
313
|
id: "cite42_youtube_trends",
|
|
314
|
+
mcp: readOnly("Get YouTube trend data"),
|
|
204
315
|
description: `YouTube trend data for rising videos, creator angles, title patterns, channel examples, and video-market opportunities around a topic. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
205
316
|
inputSchema: import_zod.z.object({
|
|
206
317
|
topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
|
|
@@ -212,7 +323,10 @@ var aivYoutubeTrends = (0, import_tools.createTool)({
|
|
|
212
323
|
var channelEnum = import_zod.z.enum(["ai_search", "seo_keywords", "google_trends", "reddit", "youtube"]);
|
|
213
324
|
var aivFindContentOpportunities = (0, import_tools.createTool)({
|
|
214
325
|
id: "cite42_find_content_opportunities",
|
|
215
|
-
|
|
326
|
+
mcp: readOnly("Find content opportunities"),
|
|
327
|
+
description: workflowDescription(
|
|
328
|
+
"Run the content-opportunities workflow: fans out across AI search, SEO keywords, trends, Reddit, YouTube, and citations and returns the aggregated data in one response."
|
|
329
|
+
),
|
|
216
330
|
inputSchema: import_zod.z.object({
|
|
217
331
|
topic: queryText("Market, niche, product category, or audience to research."),
|
|
218
332
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -225,7 +339,10 @@ var aivFindContentOpportunities = (0, import_tools.createTool)({
|
|
|
225
339
|
});
|
|
226
340
|
var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
|
|
227
341
|
id: "cite42_analyze_topic_demand",
|
|
228
|
-
|
|
342
|
+
mcp: readOnly("Analyze topic demand"),
|
|
343
|
+
description: workflowDescription(
|
|
344
|
+
"Run the topic-demand workflow: SEO keyword data, Google Trends, AI answers, and audience conversations, aggregated in one response."
|
|
345
|
+
),
|
|
229
346
|
inputSchema: import_zod.z.object({
|
|
230
347
|
topic: queryText("Topic, keyword, category, or audience demand to analyze."),
|
|
231
348
|
country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
|
|
@@ -235,7 +352,10 @@ var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
|
|
|
235
352
|
});
|
|
236
353
|
var aivMapAiPrompts = (0, import_tools.createTool)({
|
|
237
354
|
id: "cite42_map_ai_prompts",
|
|
238
|
-
|
|
355
|
+
mcp: readOnly("Map buyer prompts for AI search"),
|
|
356
|
+
description: workflowDescription(
|
|
357
|
+
"Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data."
|
|
358
|
+
),
|
|
239
359
|
inputSchema: import_zod.z.object({
|
|
240
360
|
topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
|
|
241
361
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -248,7 +368,10 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
|
|
|
248
368
|
});
|
|
249
369
|
var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
|
|
250
370
|
id: "cite42_analyze_competitor_content",
|
|
251
|
-
|
|
371
|
+
mcp: readOnly("Analyze competitor content"),
|
|
372
|
+
description: workflowDescription(
|
|
373
|
+
"Run the competitor-content workflow: AI rankings, head-to-head comparison, citations, keyword demand, and topic coverage, aggregated in one response."
|
|
374
|
+
),
|
|
252
375
|
inputSchema: import_zod.z.object({
|
|
253
376
|
competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
|
|
254
377
|
topic: import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
|
|
@@ -259,7 +382,10 @@ var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
|
|
|
259
382
|
});
|
|
260
383
|
var aivFindAiCitationGaps = (0, import_tools.createTool)({
|
|
261
384
|
id: "cite42_find_ai_citation_gaps",
|
|
262
|
-
|
|
385
|
+
mcp: readOnly("Find AI citation gaps"),
|
|
386
|
+
description: workflowDescription(
|
|
387
|
+
"Run the AI-citation-gaps workflow: find queries where AI answers cite others but not your brand, domain, or URL, with the supporting data."
|
|
388
|
+
),
|
|
263
389
|
inputSchema: import_zod.z.object({
|
|
264
390
|
query: queryText("AI-search query to inspect for citation gaps."),
|
|
265
391
|
brand: shortText("Brand or domain that should earn citations."),
|
|
@@ -271,7 +397,10 @@ var aivFindAiCitationGaps = (0, import_tools.createTool)({
|
|
|
271
397
|
});
|
|
272
398
|
var aivGenerateContentBrief = (0, import_tools.createTool)({
|
|
273
399
|
id: "cite42_generate_content_brief",
|
|
274
|
-
|
|
400
|
+
mcp: readOnly("Generate content brief data"),
|
|
401
|
+
description: workflowDescription(
|
|
402
|
+
"Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data."
|
|
403
|
+
),
|
|
275
404
|
inputSchema: import_zod.z.object({
|
|
276
405
|
topic: queryText("Topic, keyword, or content idea to brief."),
|
|
277
406
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -281,6 +410,182 @@ var aivGenerateContentBrief = (0, import_tools.createTool)({
|
|
|
281
410
|
}),
|
|
282
411
|
execute: async (input) => aivPost("/workflows/content-brief", input)
|
|
283
412
|
});
|
|
413
|
+
var trackerIdInput = import_zod.z.string().trim().min(1).max(128).describe("Cite42 tracker ID returned by a tracker management or report tool.");
|
|
414
|
+
var trackerCadenceInput = import_zod.z.object({
|
|
415
|
+
frequency: import_zod.z.enum(["daily", "weekly"]),
|
|
416
|
+
timezone: import_zod.z.string().trim().min(1).max(100).refine((timezone) => {
|
|
417
|
+
try {
|
|
418
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
419
|
+
return true;
|
|
420
|
+
} catch {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
}, "Timezone must be a valid IANA timezone.").describe("IANA timezone, for example Europe/London or America/New_York."),
|
|
424
|
+
time: import_zod.z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe("Local run time in 24-hour HH:mm format."),
|
|
425
|
+
weekday: import_zod.z.number().int().min(0).max(6).optional().describe("Weekly day, where 0 is Sunday and 6 is Saturday.")
|
|
426
|
+
}).superRefine((cadence, context) => {
|
|
427
|
+
if (cadence.frequency === "weekly" && cadence.weekday === void 0) {
|
|
428
|
+
context.addIssue({
|
|
429
|
+
code: "custom",
|
|
430
|
+
path: ["weekday"],
|
|
431
|
+
message: "weekday is required for a weekly tracker"
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
var trackerItemInput = import_zod.z.discriminatedUnion("primitive", [
|
|
436
|
+
import_zod.z.object({
|
|
437
|
+
primitive: import_zod.z.literal("rankings"),
|
|
438
|
+
input: import_zod.z.object({
|
|
439
|
+
query: queryText("Prompt to measure brand visibility for."),
|
|
440
|
+
competitors: competitorList,
|
|
441
|
+
models: modelsInput
|
|
442
|
+
})
|
|
443
|
+
}),
|
|
444
|
+
import_zod.z.object({
|
|
445
|
+
primitive: import_zod.z.literal("compare"),
|
|
446
|
+
input: import_zod.z.object({
|
|
447
|
+
query: queryText("Prompt to compare the brand and competitors for."),
|
|
448
|
+
brand: shortText("Primary brand to compare."),
|
|
449
|
+
competitors: import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(10),
|
|
450
|
+
models: modelsInput
|
|
451
|
+
})
|
|
452
|
+
}),
|
|
453
|
+
import_zod.z.object({
|
|
454
|
+
primitive: import_zod.z.literal("citations"),
|
|
455
|
+
input: import_zod.z.object({
|
|
456
|
+
query: queryText("Prompt whose cited sources should be tracked."),
|
|
457
|
+
url: import_zod.z.string().trim().max(500).optional(),
|
|
458
|
+
models: modelsInput
|
|
459
|
+
})
|
|
460
|
+
}),
|
|
461
|
+
import_zod.z.object({
|
|
462
|
+
primitive: import_zod.z.literal("sentiment"),
|
|
463
|
+
input: import_zod.z.object({
|
|
464
|
+
query: queryText("Prompt whose brand sentiment should be tracked."),
|
|
465
|
+
brand: shortText("Brand whose sentiment should be scored."),
|
|
466
|
+
models: modelsInput
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
]);
|
|
470
|
+
var trackerDefinitionFields = {
|
|
471
|
+
name: import_zod.z.string().trim().min(1).max(100),
|
|
472
|
+
cadence: trackerCadenceInput,
|
|
473
|
+
items: import_zod.z.array(trackerItemInput).min(1).max(25).describe("Primitive calls to execute and store on each scheduled run.")
|
|
474
|
+
};
|
|
475
|
+
var TRACKER_BILLING_DESCRIPTION = "Tracker management and stored-report reads are free. Each scheduled or manual run bills the underlying primitive calls at their normal per-model rates; the activation response and tracker record expose the accepted maximum cost per run.";
|
|
476
|
+
var aivTrackerCreate = (0, import_tools.createTool)({
|
|
477
|
+
id: "cite42_tracker_create",
|
|
478
|
+
mcp: mutation("Create Cite42 tracker draft"),
|
|
479
|
+
description: `Create a draft daily or weekly tracker from ordinary rankings, compare, citations, and sentiment primitive inputs. Creation does not start recurring calls. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
480
|
+
inputSchema: import_zod.z.object(trackerDefinitionFields),
|
|
481
|
+
execute: async (input) => aivPost("/trackers", input)
|
|
482
|
+
});
|
|
483
|
+
var aivTrackerUpdate = (0, import_tools.createTool)({
|
|
484
|
+
id: "cite42_tracker_update",
|
|
485
|
+
mcp: mutation("Update Cite42 tracker", { destructiveHint: true }),
|
|
486
|
+
description: `Replace a tracker configuration with a validated name, cadence, and primitive item set. An edit returns the tracker to draft so its new recurring cost can be confirmed. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
487
|
+
inputSchema: import_zod.z.object({ trackerId: trackerIdInput, ...trackerDefinitionFields }),
|
|
488
|
+
execute: async ({ trackerId, ...input }) => aivPatch(`/trackers/${encodeURIComponent(trackerId)}`, input)
|
|
489
|
+
});
|
|
490
|
+
var aivTrackerActivate = (0, import_tools.createTool)({
|
|
491
|
+
id: "cite42_tracker_activate",
|
|
492
|
+
mcp: mutation("Activate Cite42 tracker", {
|
|
493
|
+
openWorldHint: true,
|
|
494
|
+
idempotentHint: true
|
|
495
|
+
}),
|
|
496
|
+
description: `Activate or resume recurring daily or weekly primitive calls. maxCostMicroPerRun must equal the tracker estimate being accepted; activation authorizes future billed calls until the tracker is paused. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
497
|
+
inputSchema: import_zod.z.object({
|
|
498
|
+
trackerId: trackerIdInput,
|
|
499
|
+
maxCostMicroPerRun: import_zod.z.number().int().nonnegative().describe("Exact estimatedCostMicroPerRun returned by the latest tracker record.")
|
|
500
|
+
}),
|
|
501
|
+
execute: async ({ trackerId, maxCostMicroPerRun }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/activate`, { maxCostMicroPerRun })
|
|
502
|
+
});
|
|
503
|
+
var aivTrackerPause = (0, import_tools.createTool)({
|
|
504
|
+
id: "cite42_tracker_pause",
|
|
505
|
+
mcp: mutation("Pause Cite42 tracker", { idempotentHint: true }),
|
|
506
|
+
description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free: costs 0 credits.",
|
|
507
|
+
inputSchema: import_zod.z.object({ trackerId: trackerIdInput }),
|
|
508
|
+
execute: async ({ trackerId }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/pause`, {})
|
|
509
|
+
});
|
|
510
|
+
var aivTrackerDelete = (0, import_tools.createTool)({
|
|
511
|
+
id: "cite42_tracker_delete",
|
|
512
|
+
mcp: mutation("Permanently delete Cite42 tracker", {
|
|
513
|
+
destructiveHint: true,
|
|
514
|
+
idempotentHint: true
|
|
515
|
+
}),
|
|
516
|
+
description: "Permanently delete a draft or paused tracker, including its saved configuration and run history. Active trackers must be paused first, and deletion is rejected while a run is queued or running. Financial usage and credit ledger records are retained. Free: costs 0 credits.",
|
|
517
|
+
inputSchema: import_zod.z.object({ trackerId: trackerIdInput }),
|
|
518
|
+
execute: async ({ trackerId }) => aivDelete(`/trackers/${encodeURIComponent(trackerId)}`)
|
|
519
|
+
});
|
|
520
|
+
var aivTrackerRun = (0, import_tools.createTool)({
|
|
521
|
+
id: "cite42_tracker_run",
|
|
522
|
+
mcp: mutation("Run Cite42 tracker now", {
|
|
523
|
+
openWorldHint: true,
|
|
524
|
+
idempotentHint: true
|
|
525
|
+
}),
|
|
526
|
+
description: `Queue one manual run using the tracker\u2019s saved primitive inputs. A repeated requestToken returns the same logical run. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
527
|
+
inputSchema: import_zod.z.object({
|
|
528
|
+
trackerId: trackerIdInput,
|
|
529
|
+
requestToken: import_zod.z.string().trim().min(1).max(200).describe("Stable caller-generated token used to deduplicate a retried manual run.")
|
|
530
|
+
}),
|
|
531
|
+
execute: async ({ trackerId, requestToken }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/runs`, { requestToken })
|
|
532
|
+
});
|
|
533
|
+
var aivTrackers = (0, import_tools.createTool)({
|
|
534
|
+
id: "cite42_trackers",
|
|
535
|
+
mcp: readOnly("List Cite42 trackers", false),
|
|
536
|
+
description: "List the connected account\u2019s trackers with status, cadence, primitive configuration, cost estimate, accepted per-run maximum, and next/last run times. Free: costs 0 credits.",
|
|
537
|
+
inputSchema: import_zod.z.object({}),
|
|
538
|
+
execute: async () => aivGet("/trackers")
|
|
539
|
+
});
|
|
540
|
+
var aivTrackerReport = (0, import_tools.createTool)({
|
|
541
|
+
id: "cite42_tracker_report",
|
|
542
|
+
mcp: readOnly("Read Cite42 tracker report", false),
|
|
543
|
+
description: "Read bounded stored run history for one tracker, including per-primitive results, changes from the prior comparable observation, statuses, sampled times, and costs. Free: costs 0 credits.",
|
|
544
|
+
inputSchema: import_zod.z.object({
|
|
545
|
+
trackerId: trackerIdInput,
|
|
546
|
+
limit: import_zod.z.number().int().min(1).max(20).optional().default(8).describe("Most recent runs to return, capped for LLM context.")
|
|
547
|
+
}),
|
|
548
|
+
execute: async ({ trackerId, limit }) => aivGet(`/trackers/${encodeURIComponent(trackerId)}/report?limit=${limit}`)
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// src/server.ts
|
|
552
|
+
var CITE42_TOOLS = {
|
|
553
|
+
cite42_credits: aivCredits,
|
|
554
|
+
cite42_pricing: aivPricing,
|
|
555
|
+
cite42_usage: aivUsage,
|
|
556
|
+
cite42_search: aivSearch,
|
|
557
|
+
cite42_rankings: aivRankings,
|
|
558
|
+
cite42_compare: aivCompare,
|
|
559
|
+
cite42_citations: aivCitations,
|
|
560
|
+
cite42_sentiment: aivSentiment,
|
|
561
|
+
cite42_keywords: aivKeywords,
|
|
562
|
+
cite42_trends: aivTrends,
|
|
563
|
+
cite42_reddit_trends: aivRedditTrends,
|
|
564
|
+
cite42_youtube_trends: aivYoutubeTrends,
|
|
565
|
+
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
566
|
+
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
567
|
+
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
568
|
+
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
569
|
+
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
570
|
+
cite42_generate_content_brief: aivGenerateContentBrief,
|
|
571
|
+
cite42_tracker_create: aivTrackerCreate,
|
|
572
|
+
cite42_tracker_update: aivTrackerUpdate,
|
|
573
|
+
cite42_tracker_activate: aivTrackerActivate,
|
|
574
|
+
cite42_tracker_pause: aivTrackerPause,
|
|
575
|
+
cite42_tracker_delete: aivTrackerDelete,
|
|
576
|
+
cite42_tracker_run: aivTrackerRun,
|
|
577
|
+
cite42_trackers: aivTrackers,
|
|
578
|
+
cite42_tracker_report: aivTrackerReport
|
|
579
|
+
};
|
|
580
|
+
function buildCite42Server(version) {
|
|
581
|
+
return new import_mcp.MCPServer({
|
|
582
|
+
id: "cite42",
|
|
583
|
+
name: "Cite42",
|
|
584
|
+
version,
|
|
585
|
+
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
586
|
+
tools: CITE42_TOOLS
|
|
587
|
+
});
|
|
588
|
+
}
|
|
284
589
|
|
|
285
590
|
// src/stdio.ts
|
|
286
591
|
if (!resolveApiKey()) {
|
|
@@ -289,30 +594,7 @@ if (!resolveApiKey()) {
|
|
|
289
594
|
);
|
|
290
595
|
process.exit(1);
|
|
291
596
|
}
|
|
292
|
-
|
|
293
|
-
id: "cite42",
|
|
294
|
-
name: "Cite42",
|
|
295
|
-
version: "0.1.15",
|
|
296
|
-
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
297
|
-
tools: {
|
|
298
|
-
cite42_search: aivSearch,
|
|
299
|
-
cite42_rankings: aivRankings,
|
|
300
|
-
cite42_compare: aivCompare,
|
|
301
|
-
cite42_citations: aivCitations,
|
|
302
|
-
cite42_sentiment: aivSentiment,
|
|
303
|
-
cite42_keywords: aivKeywords,
|
|
304
|
-
cite42_trends: aivTrends,
|
|
305
|
-
cite42_reddit_trends: aivRedditTrends,
|
|
306
|
-
cite42_youtube_trends: aivYoutubeTrends,
|
|
307
|
-
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
308
|
-
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
309
|
-
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
310
|
-
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
311
|
-
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
312
|
-
cite42_generate_content_brief: aivGenerateContentBrief
|
|
313
|
-
}
|
|
314
|
-
});
|
|
315
|
-
server.startStdio().catch((error) => {
|
|
597
|
+
buildCite42Server("0.1.17").startStdio().catch((error) => {
|
|
316
598
|
console.error("[@cite42/mcp] Fatal error:", error);
|
|
317
599
|
process.exit(1);
|
|
318
600
|
});
|
package/dist/stdio.mjs
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
// src/stdio.ts
|
|
4
|
-
import { MCPServer } from "@mastra/mcp";
|
|
5
|
-
|
|
6
3
|
// src/client.ts
|
|
7
4
|
var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
|
|
8
5
|
var MICRO_CREDITS_PER_CREDIT = 1e3;
|
|
@@ -39,6 +36,14 @@ function toCite42McpResult(value) {
|
|
|
39
36
|
}
|
|
40
37
|
};
|
|
41
38
|
}
|
|
39
|
+
function toCite42CreditsBalance(balanceMicroCredits) {
|
|
40
|
+
return {
|
|
41
|
+
cite42Credits: {
|
|
42
|
+
remaining: toCredits(balanceMicroCredits),
|
|
43
|
+
message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
42
47
|
function errorMessageForMcp(status, path, body) {
|
|
43
48
|
try {
|
|
44
49
|
const payload = JSON.parse(body);
|
|
@@ -59,6 +64,7 @@ function readFlag(name) {
|
|
|
59
64
|
const prefix = `--${name}=`;
|
|
60
65
|
for (let i = 0; i < argv.length; i++) {
|
|
61
66
|
const arg = argv[i];
|
|
67
|
+
if (!arg) continue;
|
|
62
68
|
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
63
69
|
if (arg === `--${name}` && i + 1 < argv.length) return argv[i + 1];
|
|
64
70
|
}
|
|
@@ -80,9 +86,27 @@ function getBaseUrl() {
|
|
|
80
86
|
return readFlag("api-base") ?? process.env.CITE42_API_BASE ?? DEFAULT_BASE_URL;
|
|
81
87
|
}
|
|
82
88
|
async function aivPost(path, body) {
|
|
89
|
+
return aivWrite("POST", path, body);
|
|
90
|
+
}
|
|
91
|
+
async function aivPatch(path, body) {
|
|
92
|
+
return aivWrite("PATCH", path, body);
|
|
93
|
+
}
|
|
94
|
+
async function aivDelete(path) {
|
|
83
95
|
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
84
96
|
const res = await fetch(url, {
|
|
85
|
-
method: "
|
|
97
|
+
method: "DELETE",
|
|
98
|
+
headers: { Authorization: `Bearer ${getApiKey()}` }
|
|
99
|
+
});
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
const body = await res.text();
|
|
102
|
+
throw new Error(errorMessageForMcp(res.status, path, body));
|
|
103
|
+
}
|
|
104
|
+
return toCite42McpResult(await res.json());
|
|
105
|
+
}
|
|
106
|
+
async function aivWrite(method, path, body) {
|
|
107
|
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
108
|
+
const res = await fetch(url, {
|
|
109
|
+
method,
|
|
86
110
|
headers: {
|
|
87
111
|
"Content-Type": "application/json",
|
|
88
112
|
Authorization: `Bearer ${getApiKey()}`
|
|
@@ -95,6 +119,21 @@ async function aivPost(path, body) {
|
|
|
95
119
|
}
|
|
96
120
|
return toCite42McpResult(await res.json());
|
|
97
121
|
}
|
|
122
|
+
async function aivGet(path) {
|
|
123
|
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
124
|
+
const res = await fetch(url, {
|
|
125
|
+
method: "GET",
|
|
126
|
+
headers: { Authorization: `Bearer ${getApiKey()}` }
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const text = await res.text();
|
|
130
|
+
throw new Error(errorMessageForMcp(res.status, path, text));
|
|
131
|
+
}
|
|
132
|
+
return toCite42McpResult(await res.json());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/server.ts
|
|
136
|
+
import { MCPServer } from "@mastra/mcp";
|
|
98
137
|
|
|
99
138
|
// src/tools.ts
|
|
100
139
|
import { createTool } from "@mastra/core/tools";
|
|
@@ -104,9 +143,27 @@ var QUERY_MAX_CHARS = 2e3;
|
|
|
104
143
|
var SHORT_TEXT_MAX_CHARS = 200;
|
|
105
144
|
var TIMEFRAME_MAX_CHARS = 80;
|
|
106
145
|
var MODEL_SELECTION_DESCRIPTION = 'Optional model subset. Omit to query all five AI surfaces (chatgpt, claude, perplexity, gemini, google_ai_overview). Pass ["perplexity"] or ["perplexity","claude"] when the user asks for specific surfaces or wants to reduce cost.';
|
|
107
|
-
var MCP_USAGE_GUIDANCE = "
|
|
108
|
-
var MODEL_PRICING = "Pricing
|
|
109
|
-
var DATA_PRICING = "
|
|
146
|
+
var MCP_USAGE_GUIDANCE = "Covers AI-search, citation, SEO, keyword, trend, and market-research data. If a plan needs more than 20 endpoint calls or may use more than 500 Cite42 credits, estimate the credits and ask the user before continuing. When a result contains cite42Credits, that figure is Cite42 credits, not a model or host balance. Free (0 credits): cite42_credits for the remaining balance, cite42_pricing for exact per-tool prices and cost estimates, cite42_usage for recent calls.";
|
|
147
|
+
var MODEL_PRICING = "Pricing is per selected AI surface; omitting models queries all five. Use the models field when the user names a surface or asks to reduce credit use, and call cite42_pricing for exact current rates.";
|
|
148
|
+
var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
|
|
149
|
+
var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the primitive endpoints that run successfully and the selected model rates.";
|
|
150
|
+
function workflowDescription(description) {
|
|
151
|
+
return `${description} ${MCP_USAGE_GUIDANCE} ${MODEL_PRICING} ${DATA_PRICING} ${WORKFLOW_PRICING}`;
|
|
152
|
+
}
|
|
153
|
+
function readOnly(title, openWorldHint = true) {
|
|
154
|
+
return { annotations: { title, readOnlyHint: true, openWorldHint } };
|
|
155
|
+
}
|
|
156
|
+
function mutation(title, options = {}) {
|
|
157
|
+
return {
|
|
158
|
+
annotations: {
|
|
159
|
+
title,
|
|
160
|
+
readOnlyHint: false,
|
|
161
|
+
destructiveHint: options.destructiveHint ?? false,
|
|
162
|
+
openWorldHint: options.openWorldHint ?? false,
|
|
163
|
+
...options.idempotentHint === void 0 ? {} : { idempotentHint: options.idempotentHint }
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
}
|
|
110
167
|
function queryText(description) {
|
|
111
168
|
return z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
|
|
112
169
|
}
|
|
@@ -118,8 +175,52 @@ function optionalShortText(description) {
|
|
|
118
175
|
}
|
|
119
176
|
var modelsInput = z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
|
|
120
177
|
var competitorList = z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
|
|
178
|
+
var aivCredits = createTool({
|
|
179
|
+
id: "cite42_credits",
|
|
180
|
+
mcp: readOnly("Check Cite42 credit balance", false),
|
|
181
|
+
description: "Check the remaining Cite42 credit balance for the connected account. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
|
|
182
|
+
inputSchema: z.object({}),
|
|
183
|
+
execute: async () => {
|
|
184
|
+
const usage = await aivGet("/usage");
|
|
185
|
+
const microCredits = usage.balance?.microCredits;
|
|
186
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
187
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
188
|
+
}
|
|
189
|
+
return toCite42CreditsBalance(microCredits);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
var aivPricing = createTool({
|
|
193
|
+
id: "cite42_pricing",
|
|
194
|
+
mcp: readOnly("Get Cite42 price table", false),
|
|
195
|
+
description: 'Get the exact Cite42 price table in credits: per-model rates, flat-rate tools, workflow maximums, and which tools are free. Use it to answer any "how much does X cost" question or to estimate a plan before running it. Free: costs 0 credits. ' + MCP_USAGE_GUIDANCE,
|
|
196
|
+
inputSchema: z.object({}),
|
|
197
|
+
execute: async () => aivGet("/pricing")
|
|
198
|
+
});
|
|
199
|
+
var aivUsage = createTool({
|
|
200
|
+
id: "cite42_usage",
|
|
201
|
+
mcp: readOnly("Get recent Cite42 usage", false),
|
|
202
|
+
description: "Get recent Cite42 usage for the connected account: the last calls with their cost and status, plus the remaining credit balance. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
|
|
203
|
+
inputSchema: z.object({}),
|
|
204
|
+
execute: async () => {
|
|
205
|
+
const usage = await aivGet("/usage");
|
|
206
|
+
const microCredits = usage.balance?.microCredits;
|
|
207
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
208
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
...toCite42CreditsBalance(microCredits),
|
|
212
|
+
recentCalls: (usage.recent ?? []).slice(0, 20).map((row) => ({
|
|
213
|
+
endpoint: row.endpoint,
|
|
214
|
+
status: row.status,
|
|
215
|
+
credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
|
|
216
|
+
at: row.createdAt
|
|
217
|
+
}))
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
121
221
|
var aivSearch = createTool({
|
|
122
222
|
id: "cite42_search",
|
|
223
|
+
mcp: readOnly("Search AI models"),
|
|
123
224
|
description: `Run a query against AI models (ChatGPT, Claude, Perplexity, Gemini, and Google AI Overviews) and return each model's answer along with the sources it cited. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
124
225
|
inputSchema: z.object({
|
|
125
226
|
query: queryText("The concise AI-search/market-research query to run against the selected models."),
|
|
@@ -129,6 +230,7 @@ var aivSearch = createTool({
|
|
|
129
230
|
});
|
|
130
231
|
var aivCitations = createTool({
|
|
131
232
|
id: "cite42_citations",
|
|
233
|
+
mcp: readOnly("Find AI citations"),
|
|
132
234
|
description: `Find which sources AI models cite for a query. Aggregates cited URLs across models with per-model counts; optionally checks whether a specific URL is cited. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
133
235
|
inputSchema: z.object({
|
|
134
236
|
query: queryText("The concise query to look up cited sources for."),
|
|
@@ -139,6 +241,7 @@ var aivCitations = createTool({
|
|
|
139
241
|
});
|
|
140
242
|
var aivRankings = createTool({
|
|
141
243
|
id: "cite42_rankings",
|
|
244
|
+
mcp: readOnly("Measure AI brand rankings"),
|
|
142
245
|
description: `Measure how a set of brands/competitors rank in AI answers for a query: mention rate, average position, and a per-model breakdown across ChatGPT, Claude, Perplexity, Gemini, and Google AI Overviews. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
143
246
|
inputSchema: z.object({
|
|
144
247
|
query: queryText("The concise query to measure brand/competitor visibility on."),
|
|
@@ -149,6 +252,7 @@ var aivRankings = createTool({
|
|
|
149
252
|
});
|
|
150
253
|
var aivCompare = createTool({
|
|
151
254
|
id: "cite42_compare",
|
|
255
|
+
mcp: readOnly("Compare brand against competitors"),
|
|
152
256
|
description: `Compare one primary brand head-to-head against a competitor set in AI answers: per-brand rankings plus a co-mention graph of which brands the models name together. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
153
257
|
inputSchema: z.object({
|
|
154
258
|
query: queryText("The concise query to compare the brand and competitors on."),
|
|
@@ -160,6 +264,7 @@ var aivCompare = createTool({
|
|
|
160
264
|
});
|
|
161
265
|
var aivSentiment = createTool({
|
|
162
266
|
id: "cite42_sentiment",
|
|
267
|
+
mcp: readOnly("Score brand sentiment in AI answers"),
|
|
163
268
|
description: `Score how each AI model talks about a brand for a query: a positive/neutral/negative split per model with the verbatim phrases driving the tone, plus an aggregate across models. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
164
269
|
inputSchema: z.object({
|
|
165
270
|
query: queryText("The concise query whose answers are scored for brand sentiment."),
|
|
@@ -170,26 +275,31 @@ var aivSentiment = createTool({
|
|
|
170
275
|
});
|
|
171
276
|
var aivKeywords = createTool({
|
|
172
277
|
id: "cite42_keywords",
|
|
173
|
-
|
|
278
|
+
mcp: readOnly("Get SEO keyword data"),
|
|
279
|
+
description: `Classic keyword data for a seed term. Omit ideas when the user only needs that seed's monthly search volume, CPC, or competition. Set ideas:true only when the user asks for related, alternative, or long-tail suggestions from provider candidates. A flat-priced data tool, not a multi-model AI-search query. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
174
280
|
inputSchema: z.object({
|
|
175
281
|
seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
|
|
176
282
|
country: z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
|
|
177
|
-
ideas: z.boolean().optional().describe(
|
|
283
|
+
ideas: z.boolean().optional().describe(
|
|
284
|
+
"Omit for volume, CPC, and competition on the seed only. Set true only for related, alternative, or long-tail keyword ideas."
|
|
285
|
+
)
|
|
178
286
|
}),
|
|
179
287
|
execute: async (input) => aivPost("/keywords", input)
|
|
180
288
|
});
|
|
181
289
|
var aivTrends = createTool({
|
|
182
290
|
id: "cite42_trends",
|
|
291
|
+
mcp: readOnly("Get Google Trends data"),
|
|
183
292
|
description: `Google Trends interest-over-time, related and rising queries, and a rising/falling/stable label for a term. A flat-priced data tool, not an AI-model query. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
184
293
|
inputSchema: z.object({
|
|
185
294
|
term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
|
|
186
295
|
timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trends window, e.g. "past_12_months", "past_30_days", "past_5_years".'),
|
|
187
|
-
geo: z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United
|
|
296
|
+
geo: z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United Kingdom"). Defaults to the United States when omitted.')
|
|
188
297
|
}),
|
|
189
298
|
execute: async (input) => aivPost("/trends", input)
|
|
190
299
|
});
|
|
191
300
|
var aivRedditTrends = createTool({
|
|
192
301
|
id: "cite42_reddit_trends",
|
|
302
|
+
mcp: readOnly("Get Reddit trend data"),
|
|
193
303
|
description: `Reddit trend data for audience questions, pain points, rising threads, repeated phrases, mentioned products, and source URLs around a topic. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
194
304
|
inputSchema: z.object({
|
|
195
305
|
topic: queryText("Topic, category, product, or audience to research on Reddit."),
|
|
@@ -200,6 +310,7 @@ var aivRedditTrends = createTool({
|
|
|
200
310
|
});
|
|
201
311
|
var aivYoutubeTrends = createTool({
|
|
202
312
|
id: "cite42_youtube_trends",
|
|
313
|
+
mcp: readOnly("Get YouTube trend data"),
|
|
203
314
|
description: `YouTube trend data for rising videos, creator angles, title patterns, channel examples, and video-market opportunities around a topic. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
|
|
204
315
|
inputSchema: z.object({
|
|
205
316
|
topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
|
|
@@ -211,7 +322,10 @@ var aivYoutubeTrends = createTool({
|
|
|
211
322
|
var channelEnum = z.enum(["ai_search", "seo_keywords", "google_trends", "reddit", "youtube"]);
|
|
212
323
|
var aivFindContentOpportunities = createTool({
|
|
213
324
|
id: "cite42_find_content_opportunities",
|
|
214
|
-
|
|
325
|
+
mcp: readOnly("Find content opportunities"),
|
|
326
|
+
description: workflowDescription(
|
|
327
|
+
"Run the content-opportunities workflow: fans out across AI search, SEO keywords, trends, Reddit, YouTube, and citations and returns the aggregated data in one response."
|
|
328
|
+
),
|
|
215
329
|
inputSchema: z.object({
|
|
216
330
|
topic: queryText("Market, niche, product category, or audience to research."),
|
|
217
331
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -224,7 +338,10 @@ var aivFindContentOpportunities = createTool({
|
|
|
224
338
|
});
|
|
225
339
|
var aivAnalyzeTopicDemand = createTool({
|
|
226
340
|
id: "cite42_analyze_topic_demand",
|
|
227
|
-
|
|
341
|
+
mcp: readOnly("Analyze topic demand"),
|
|
342
|
+
description: workflowDescription(
|
|
343
|
+
"Run the topic-demand workflow: SEO keyword data, Google Trends, AI answers, and audience conversations, aggregated in one response."
|
|
344
|
+
),
|
|
228
345
|
inputSchema: z.object({
|
|
229
346
|
topic: queryText("Topic, keyword, category, or audience demand to analyze."),
|
|
230
347
|
country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
|
|
@@ -234,7 +351,10 @@ var aivAnalyzeTopicDemand = createTool({
|
|
|
234
351
|
});
|
|
235
352
|
var aivMapAiPrompts = createTool({
|
|
236
353
|
id: "cite42_map_ai_prompts",
|
|
237
|
-
|
|
354
|
+
mcp: readOnly("Map buyer prompts for AI search"),
|
|
355
|
+
description: workflowDescription(
|
|
356
|
+
"Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data."
|
|
357
|
+
),
|
|
238
358
|
inputSchema: z.object({
|
|
239
359
|
topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
|
|
240
360
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -247,7 +367,10 @@ var aivMapAiPrompts = createTool({
|
|
|
247
367
|
});
|
|
248
368
|
var aivAnalyzeCompetitorContent = createTool({
|
|
249
369
|
id: "cite42_analyze_competitor_content",
|
|
250
|
-
|
|
370
|
+
mcp: readOnly("Analyze competitor content"),
|
|
371
|
+
description: workflowDescription(
|
|
372
|
+
"Run the competitor-content workflow: AI rankings, head-to-head comparison, citations, keyword demand, and topic coverage, aggregated in one response."
|
|
373
|
+
),
|
|
251
374
|
inputSchema: z.object({
|
|
252
375
|
competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
|
|
253
376
|
topic: z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
|
|
@@ -258,7 +381,10 @@ var aivAnalyzeCompetitorContent = createTool({
|
|
|
258
381
|
});
|
|
259
382
|
var aivFindAiCitationGaps = createTool({
|
|
260
383
|
id: "cite42_find_ai_citation_gaps",
|
|
261
|
-
|
|
384
|
+
mcp: readOnly("Find AI citation gaps"),
|
|
385
|
+
description: workflowDescription(
|
|
386
|
+
"Run the AI-citation-gaps workflow: find queries where AI answers cite others but not your brand, domain, or URL, with the supporting data."
|
|
387
|
+
),
|
|
262
388
|
inputSchema: z.object({
|
|
263
389
|
query: queryText("AI-search query to inspect for citation gaps."),
|
|
264
390
|
brand: shortText("Brand or domain that should earn citations."),
|
|
@@ -270,7 +396,10 @@ var aivFindAiCitationGaps = createTool({
|
|
|
270
396
|
});
|
|
271
397
|
var aivGenerateContentBrief = createTool({
|
|
272
398
|
id: "cite42_generate_content_brief",
|
|
273
|
-
|
|
399
|
+
mcp: readOnly("Generate content brief data"),
|
|
400
|
+
description: workflowDescription(
|
|
401
|
+
"Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data."
|
|
402
|
+
),
|
|
274
403
|
inputSchema: z.object({
|
|
275
404
|
topic: queryText("Topic, keyword, or content idea to brief."),
|
|
276
405
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -280,6 +409,182 @@ var aivGenerateContentBrief = createTool({
|
|
|
280
409
|
}),
|
|
281
410
|
execute: async (input) => aivPost("/workflows/content-brief", input)
|
|
282
411
|
});
|
|
412
|
+
var trackerIdInput = z.string().trim().min(1).max(128).describe("Cite42 tracker ID returned by a tracker management or report tool.");
|
|
413
|
+
var trackerCadenceInput = z.object({
|
|
414
|
+
frequency: z.enum(["daily", "weekly"]),
|
|
415
|
+
timezone: z.string().trim().min(1).max(100).refine((timezone) => {
|
|
416
|
+
try {
|
|
417
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
418
|
+
return true;
|
|
419
|
+
} catch {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}, "Timezone must be a valid IANA timezone.").describe("IANA timezone, for example Europe/London or America/New_York."),
|
|
423
|
+
time: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe("Local run time in 24-hour HH:mm format."),
|
|
424
|
+
weekday: z.number().int().min(0).max(6).optional().describe("Weekly day, where 0 is Sunday and 6 is Saturday.")
|
|
425
|
+
}).superRefine((cadence, context) => {
|
|
426
|
+
if (cadence.frequency === "weekly" && cadence.weekday === void 0) {
|
|
427
|
+
context.addIssue({
|
|
428
|
+
code: "custom",
|
|
429
|
+
path: ["weekday"],
|
|
430
|
+
message: "weekday is required for a weekly tracker"
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
var trackerItemInput = z.discriminatedUnion("primitive", [
|
|
435
|
+
z.object({
|
|
436
|
+
primitive: z.literal("rankings"),
|
|
437
|
+
input: z.object({
|
|
438
|
+
query: queryText("Prompt to measure brand visibility for."),
|
|
439
|
+
competitors: competitorList,
|
|
440
|
+
models: modelsInput
|
|
441
|
+
})
|
|
442
|
+
}),
|
|
443
|
+
z.object({
|
|
444
|
+
primitive: z.literal("compare"),
|
|
445
|
+
input: z.object({
|
|
446
|
+
query: queryText("Prompt to compare the brand and competitors for."),
|
|
447
|
+
brand: shortText("Primary brand to compare."),
|
|
448
|
+
competitors: z.array(shortText("Competitor brand/domain name.")).nonempty().max(10),
|
|
449
|
+
models: modelsInput
|
|
450
|
+
})
|
|
451
|
+
}),
|
|
452
|
+
z.object({
|
|
453
|
+
primitive: z.literal("citations"),
|
|
454
|
+
input: z.object({
|
|
455
|
+
query: queryText("Prompt whose cited sources should be tracked."),
|
|
456
|
+
url: z.string().trim().max(500).optional(),
|
|
457
|
+
models: modelsInput
|
|
458
|
+
})
|
|
459
|
+
}),
|
|
460
|
+
z.object({
|
|
461
|
+
primitive: z.literal("sentiment"),
|
|
462
|
+
input: z.object({
|
|
463
|
+
query: queryText("Prompt whose brand sentiment should be tracked."),
|
|
464
|
+
brand: shortText("Brand whose sentiment should be scored."),
|
|
465
|
+
models: modelsInput
|
|
466
|
+
})
|
|
467
|
+
})
|
|
468
|
+
]);
|
|
469
|
+
var trackerDefinitionFields = {
|
|
470
|
+
name: z.string().trim().min(1).max(100),
|
|
471
|
+
cadence: trackerCadenceInput,
|
|
472
|
+
items: z.array(trackerItemInput).min(1).max(25).describe("Primitive calls to execute and store on each scheduled run.")
|
|
473
|
+
};
|
|
474
|
+
var TRACKER_BILLING_DESCRIPTION = "Tracker management and stored-report reads are free. Each scheduled or manual run bills the underlying primitive calls at their normal per-model rates; the activation response and tracker record expose the accepted maximum cost per run.";
|
|
475
|
+
var aivTrackerCreate = createTool({
|
|
476
|
+
id: "cite42_tracker_create",
|
|
477
|
+
mcp: mutation("Create Cite42 tracker draft"),
|
|
478
|
+
description: `Create a draft daily or weekly tracker from ordinary rankings, compare, citations, and sentiment primitive inputs. Creation does not start recurring calls. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
479
|
+
inputSchema: z.object(trackerDefinitionFields),
|
|
480
|
+
execute: async (input) => aivPost("/trackers", input)
|
|
481
|
+
});
|
|
482
|
+
var aivTrackerUpdate = createTool({
|
|
483
|
+
id: "cite42_tracker_update",
|
|
484
|
+
mcp: mutation("Update Cite42 tracker", { destructiveHint: true }),
|
|
485
|
+
description: `Replace a tracker configuration with a validated name, cadence, and primitive item set. An edit returns the tracker to draft so its new recurring cost can be confirmed. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
486
|
+
inputSchema: z.object({ trackerId: trackerIdInput, ...trackerDefinitionFields }),
|
|
487
|
+
execute: async ({ trackerId, ...input }) => aivPatch(`/trackers/${encodeURIComponent(trackerId)}`, input)
|
|
488
|
+
});
|
|
489
|
+
var aivTrackerActivate = createTool({
|
|
490
|
+
id: "cite42_tracker_activate",
|
|
491
|
+
mcp: mutation("Activate Cite42 tracker", {
|
|
492
|
+
openWorldHint: true,
|
|
493
|
+
idempotentHint: true
|
|
494
|
+
}),
|
|
495
|
+
description: `Activate or resume recurring daily or weekly primitive calls. maxCostMicroPerRun must equal the tracker estimate being accepted; activation authorizes future billed calls until the tracker is paused. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
496
|
+
inputSchema: z.object({
|
|
497
|
+
trackerId: trackerIdInput,
|
|
498
|
+
maxCostMicroPerRun: z.number().int().nonnegative().describe("Exact estimatedCostMicroPerRun returned by the latest tracker record.")
|
|
499
|
+
}),
|
|
500
|
+
execute: async ({ trackerId, maxCostMicroPerRun }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/activate`, { maxCostMicroPerRun })
|
|
501
|
+
});
|
|
502
|
+
var aivTrackerPause = createTool({
|
|
503
|
+
id: "cite42_tracker_pause",
|
|
504
|
+
mcp: mutation("Pause Cite42 tracker", { idempotentHint: true }),
|
|
505
|
+
description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free: costs 0 credits.",
|
|
506
|
+
inputSchema: z.object({ trackerId: trackerIdInput }),
|
|
507
|
+
execute: async ({ trackerId }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/pause`, {})
|
|
508
|
+
});
|
|
509
|
+
var aivTrackerDelete = createTool({
|
|
510
|
+
id: "cite42_tracker_delete",
|
|
511
|
+
mcp: mutation("Permanently delete Cite42 tracker", {
|
|
512
|
+
destructiveHint: true,
|
|
513
|
+
idempotentHint: true
|
|
514
|
+
}),
|
|
515
|
+
description: "Permanently delete a draft or paused tracker, including its saved configuration and run history. Active trackers must be paused first, and deletion is rejected while a run is queued or running. Financial usage and credit ledger records are retained. Free: costs 0 credits.",
|
|
516
|
+
inputSchema: z.object({ trackerId: trackerIdInput }),
|
|
517
|
+
execute: async ({ trackerId }) => aivDelete(`/trackers/${encodeURIComponent(trackerId)}`)
|
|
518
|
+
});
|
|
519
|
+
var aivTrackerRun = createTool({
|
|
520
|
+
id: "cite42_tracker_run",
|
|
521
|
+
mcp: mutation("Run Cite42 tracker now", {
|
|
522
|
+
openWorldHint: true,
|
|
523
|
+
idempotentHint: true
|
|
524
|
+
}),
|
|
525
|
+
description: `Queue one manual run using the tracker\u2019s saved primitive inputs. A repeated requestToken returns the same logical run. ${TRACKER_BILLING_DESCRIPTION}`,
|
|
526
|
+
inputSchema: z.object({
|
|
527
|
+
trackerId: trackerIdInput,
|
|
528
|
+
requestToken: z.string().trim().min(1).max(200).describe("Stable caller-generated token used to deduplicate a retried manual run.")
|
|
529
|
+
}),
|
|
530
|
+
execute: async ({ trackerId, requestToken }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/runs`, { requestToken })
|
|
531
|
+
});
|
|
532
|
+
var aivTrackers = createTool({
|
|
533
|
+
id: "cite42_trackers",
|
|
534
|
+
mcp: readOnly("List Cite42 trackers", false),
|
|
535
|
+
description: "List the connected account\u2019s trackers with status, cadence, primitive configuration, cost estimate, accepted per-run maximum, and next/last run times. Free: costs 0 credits.",
|
|
536
|
+
inputSchema: z.object({}),
|
|
537
|
+
execute: async () => aivGet("/trackers")
|
|
538
|
+
});
|
|
539
|
+
var aivTrackerReport = createTool({
|
|
540
|
+
id: "cite42_tracker_report",
|
|
541
|
+
mcp: readOnly("Read Cite42 tracker report", false),
|
|
542
|
+
description: "Read bounded stored run history for one tracker, including per-primitive results, changes from the prior comparable observation, statuses, sampled times, and costs. Free: costs 0 credits.",
|
|
543
|
+
inputSchema: z.object({
|
|
544
|
+
trackerId: trackerIdInput,
|
|
545
|
+
limit: z.number().int().min(1).max(20).optional().default(8).describe("Most recent runs to return, capped for LLM context.")
|
|
546
|
+
}),
|
|
547
|
+
execute: async ({ trackerId, limit }) => aivGet(`/trackers/${encodeURIComponent(trackerId)}/report?limit=${limit}`)
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// src/server.ts
|
|
551
|
+
var CITE42_TOOLS = {
|
|
552
|
+
cite42_credits: aivCredits,
|
|
553
|
+
cite42_pricing: aivPricing,
|
|
554
|
+
cite42_usage: aivUsage,
|
|
555
|
+
cite42_search: aivSearch,
|
|
556
|
+
cite42_rankings: aivRankings,
|
|
557
|
+
cite42_compare: aivCompare,
|
|
558
|
+
cite42_citations: aivCitations,
|
|
559
|
+
cite42_sentiment: aivSentiment,
|
|
560
|
+
cite42_keywords: aivKeywords,
|
|
561
|
+
cite42_trends: aivTrends,
|
|
562
|
+
cite42_reddit_trends: aivRedditTrends,
|
|
563
|
+
cite42_youtube_trends: aivYoutubeTrends,
|
|
564
|
+
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
565
|
+
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
566
|
+
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
567
|
+
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
568
|
+
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
569
|
+
cite42_generate_content_brief: aivGenerateContentBrief,
|
|
570
|
+
cite42_tracker_create: aivTrackerCreate,
|
|
571
|
+
cite42_tracker_update: aivTrackerUpdate,
|
|
572
|
+
cite42_tracker_activate: aivTrackerActivate,
|
|
573
|
+
cite42_tracker_pause: aivTrackerPause,
|
|
574
|
+
cite42_tracker_delete: aivTrackerDelete,
|
|
575
|
+
cite42_tracker_run: aivTrackerRun,
|
|
576
|
+
cite42_trackers: aivTrackers,
|
|
577
|
+
cite42_tracker_report: aivTrackerReport
|
|
578
|
+
};
|
|
579
|
+
function buildCite42Server(version) {
|
|
580
|
+
return new MCPServer({
|
|
581
|
+
id: "cite42",
|
|
582
|
+
name: "Cite42",
|
|
583
|
+
version,
|
|
584
|
+
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
585
|
+
tools: CITE42_TOOLS
|
|
586
|
+
});
|
|
587
|
+
}
|
|
283
588
|
|
|
284
589
|
// src/stdio.ts
|
|
285
590
|
if (!resolveApiKey()) {
|
|
@@ -288,30 +593,7 @@ if (!resolveApiKey()) {
|
|
|
288
593
|
);
|
|
289
594
|
process.exit(1);
|
|
290
595
|
}
|
|
291
|
-
|
|
292
|
-
id: "cite42",
|
|
293
|
-
name: "Cite42",
|
|
294
|
-
version: "0.1.15",
|
|
295
|
-
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
296
|
-
tools: {
|
|
297
|
-
cite42_search: aivSearch,
|
|
298
|
-
cite42_rankings: aivRankings,
|
|
299
|
-
cite42_compare: aivCompare,
|
|
300
|
-
cite42_citations: aivCitations,
|
|
301
|
-
cite42_sentiment: aivSentiment,
|
|
302
|
-
cite42_keywords: aivKeywords,
|
|
303
|
-
cite42_trends: aivTrends,
|
|
304
|
-
cite42_reddit_trends: aivRedditTrends,
|
|
305
|
-
cite42_youtube_trends: aivYoutubeTrends,
|
|
306
|
-
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
307
|
-
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
308
|
-
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
309
|
-
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
310
|
-
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
311
|
-
cite42_generate_content_brief: aivGenerateContentBrief
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
server.startStdio().catch((error) => {
|
|
596
|
+
buildCite42Server("0.1.17").startStdio().catch((error) => {
|
|
315
597
|
console.error("[@cite42/mcp] Fatal error:", error);
|
|
316
598
|
process.exit(1);
|
|
317
599
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cite42/mcp",
|
|
3
3
|
"mcpName": "dev.cite42/mcp",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"description": "Cite42 MCP server: query ChatGPT, Claude, Perplexity, Gemini & Google AI Overviews for brand rankings, AI citations, SEO keyword data, and Reddit/YouTube trends. Works with Claude Code, Claude Desktop, Codex CLI, Cursor, and any MCP client.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup && chmod +x dist/stdio.mjs",
|
|
18
18
|
"dev": "tsup --watch",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest",
|
|
19
21
|
"prepublishOnly": "npm run build"
|
|
20
22
|
},
|
|
21
23
|
"dependencies": {
|
|
@@ -24,9 +26,11 @@
|
|
|
24
26
|
"zod": "^4.4.3"
|
|
25
27
|
},
|
|
26
28
|
"devDependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
30
|
"@types/node": "^20.19.27",
|
|
28
31
|
"tsup": "^8.0.0",
|
|
29
|
-
"typescript": "^5.9.3"
|
|
32
|
+
"typescript": "^5.9.3",
|
|
33
|
+
"vitest": "^4.1.10"
|
|
30
34
|
},
|
|
31
35
|
"publishConfig": {
|
|
32
36
|
"access": "public"
|