@cite42/mcp 0.1.14 → 0.1.16
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 +31 -18
- package/dist/stdio.cjs +198 -41
- package/dist/stdio.mjs +198 -41
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
@@ -14,14 +14,16 @@ Once installed, your MCP client (Claude Code, Claude Desktop, Codex CLI, Cursor,
|
|
|
14
14
|
|
|
15
15
|
## Requirements
|
|
16
16
|
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
17
|
+
- **A Cite42 account**, free to create at [cite42.dev](https://www.cite42.dev)
|
|
18
|
+
- **Node.js 18 or newer and a Cite42 API key** for Claude Code, Codex CLI, and Cursor
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
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
23
|
|
|
24
|
-
### Step 1: Get your API key
|
|
24
|
+
### Step 1: Get your API key for local clients
|
|
25
|
+
|
|
26
|
+
Skip this step for Claude Desktop; its hosted connector authorizes through sign-in.
|
|
25
27
|
|
|
26
28
|
1. Sign in at [cite42.dev](https://www.cite42.dev)
|
|
27
29
|
2. Open **Dashboard → API Keys**: [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys)
|
|
@@ -29,7 +31,7 @@ There is nothing to install manually. Every client below runs the server on dema
|
|
|
29
31
|
|
|
30
32
|
### Step 2: Add the server to your client
|
|
31
33
|
|
|
32
|
-
Pick **one** of the clients below and replace `cite42_live_your_key_here` with your real key.
|
|
34
|
+
Pick **one** of the clients below. For Claude Code, Codex CLI, and Cursor, replace `cite42_live_your_key_here` with your real key.
|
|
33
35
|
|
|
34
36
|
#### Claude Code
|
|
35
37
|
|
|
@@ -37,15 +39,29 @@ Pick **one** of the clients below and replace `cite42_live_your_key_here` with y
|
|
|
37
39
|
claude mcp add cite42 -e CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
#### Claude Desktop
|
|
43
|
+
|
|
44
|
+
1. Open **Settings → Connectors** and click **Add**.
|
|
45
|
+
2. Select **Add custom connector**.
|
|
46
|
+
3. Enter **Cite42** for the name and paste this remote MCP URL:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
https://mcp.cite42.dev
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
4. Click **Connect** and complete the Cite42 sign-in. No API key is required.
|
|
53
|
+
|
|
54
|
+
The same hosted-connector steps apply in Claude.ai.
|
|
55
|
+
|
|
40
56
|
#### Codex CLI
|
|
41
57
|
|
|
42
58
|
```bash
|
|
43
59
|
codex mcp add cite42 --env CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
|
|
44
60
|
```
|
|
45
61
|
|
|
46
|
-
####
|
|
62
|
+
#### Cursor
|
|
47
63
|
|
|
48
|
-
Add this
|
|
64
|
+
Add this under **Settings → MCP**, or save it to `~/.cursor/mcp.json`:
|
|
49
65
|
|
|
50
66
|
```json
|
|
51
67
|
{
|
|
@@ -61,30 +77,27 @@ Add this to `claude_desktop_config.json` (**Settings → Developer → Edit Conf
|
|
|
61
77
|
}
|
|
62
78
|
```
|
|
63
79
|
|
|
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
80
|
### Step 3: Restart so the server loads
|
|
69
81
|
|
|
70
|
-
MCP servers are started by the client, so
|
|
82
|
+
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
83
|
|
|
72
84
|
- **Claude Code / Codex CLI**: exit the current session and open a **new terminal** (or start a new `claude` / `codex` session)
|
|
73
|
-
- **
|
|
85
|
+
- **Cursor**: fully quit and reopen the app
|
|
74
86
|
|
|
75
|
-
You do not need to keep `npx -y @cite42/mcp` running in a separate terminal.
|
|
87
|
+
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
88
|
|
|
77
89
|
### Step 4: Verify it's running
|
|
78
90
|
|
|
79
91
|
- **Claude Code:** run `claude mcp list` and check that `cite42` shows as ✔ connected. Or type `/mcp` inside a session.
|
|
92
|
+
- **Claude Desktop:** open **Settings → Connectors** and confirm Cite42 is connected.
|
|
80
93
|
- **Codex CLI:** run `codex mcp list`.
|
|
81
|
-
- **
|
|
94
|
+
- **Cursor:** look for `cite42` in the MCP/tools indicator. 15 tools should be listed.
|
|
82
95
|
|
|
83
96
|
If it doesn't appear, see [Troubleshooting](#troubleshooting).
|
|
84
97
|
|
|
85
98
|
## Try it: example prompts
|
|
86
99
|
|
|
87
|
-
After restarting, just ask in plain language. Your agent picks the right Cite42 tool automatically.
|
|
100
|
+
After connecting or restarting, just ask in plain language. Your agent picks the right Cite42 tool automatically.
|
|
88
101
|
|
|
89
102
|
- *"Use Cite42 to check which brands AI models recommend for 'best project management software'."*
|
|
90
103
|
- *"Which sources do AI models cite when asked about CRM tools for startups? Is mycompany.com among them?"*
|
|
@@ -123,7 +136,7 @@ The exact brands and sources change by query and model. A good result should sho
|
|
|
123
136
|
|
|
124
137
|
### SEO & trend data
|
|
125
138
|
|
|
126
|
-
- `cite42_keywords`: get search volume, CPC, competition,
|
|
139
|
+
- `cite42_keywords`: get seed search volume, CPC, and competition, with optional related keyword ideas.
|
|
127
140
|
- `cite42_trends`: get Google Trends interest over time, related and rising queries, and a trend label.
|
|
128
141
|
- `cite42_reddit_trends`: find Reddit audience questions, pain points, rising threads, and product mentions.
|
|
129
142
|
- `cite42_youtube_trends`: find YouTube rising videos, creator angles, title patterns, and opportunities.
|
package/dist/stdio.cjs
CHANGED
|
@@ -1,16 +1,71 @@
|
|
|
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";
|
|
6
|
+
var MICRO_CREDITS_PER_CREDIT = 1e3;
|
|
7
|
+
function isJsonObject(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function toCredits(microCredits) {
|
|
11
|
+
return microCredits / MICRO_CREDITS_PER_CREDIT;
|
|
12
|
+
}
|
|
13
|
+
function formatCredits(microCredits) {
|
|
14
|
+
const credits = toCredits(microCredits);
|
|
15
|
+
return credits.toLocaleString(
|
|
16
|
+
"en-US",
|
|
17
|
+
Number.isInteger(credits) ? { maximumFractionDigits: 0 } : { minimumFractionDigits: 0, maximumFractionDigits: 2 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
function toCite42McpResult(value) {
|
|
21
|
+
if (!isJsonObject(value)) return value;
|
|
22
|
+
const { billing, warning, ...result } = value;
|
|
23
|
+
if (!isJsonObject(billing)) return value;
|
|
24
|
+
const cost = isJsonObject(billing.cost) ? billing.cost.microCredits : void 0;
|
|
25
|
+
const balance = isJsonObject(billing.balance) ? billing.balance.microCredits : void 0;
|
|
26
|
+
if (typeof cost !== "number" || !Number.isFinite(cost) || typeof balance !== "number" || !Number.isFinite(balance)) {
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
const lowCredits = typeof warning === "string";
|
|
30
|
+
const message = `Cite42 credits: ${formatCredits(cost)} charged; ${formatCredits(balance)} remaining.`;
|
|
31
|
+
return {
|
|
32
|
+
...result,
|
|
33
|
+
cite42Credits: {
|
|
34
|
+
charged: toCredits(cost),
|
|
35
|
+
remaining: toCredits(balance),
|
|
36
|
+
message: lowCredits ? `${message} Low Cite42 credit balance.` : message
|
|
37
|
+
}
|
|
38
|
+
};
|
|
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
|
+
}
|
|
48
|
+
function errorMessageForMcp(status, path, body) {
|
|
49
|
+
try {
|
|
50
|
+
const payload = JSON.parse(body);
|
|
51
|
+
if (isJsonObject(payload) && isJsonObject(payload.error) && typeof payload.error.code === "string") {
|
|
52
|
+
if (payload.error.code === "daily_limit_exceeded" || payload.error.code === "monthly_limit_exceeded") {
|
|
53
|
+
return "Cite42 credit limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
|
|
54
|
+
}
|
|
55
|
+
if (payload.error.code === "insufficient_credits") {
|
|
56
|
+
return "Insufficient Cite42 credits. Top up at https://www.cite42.dev/app/billing.";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
return `Cite42 API ${status} ${path}: ${body}`;
|
|
62
|
+
}
|
|
9
63
|
function readFlag(name) {
|
|
10
64
|
const argv = process.argv.slice(2);
|
|
11
65
|
const prefix = `--${name}=`;
|
|
12
66
|
for (let i = 0; i < argv.length; i++) {
|
|
13
67
|
const arg = argv[i];
|
|
68
|
+
if (!arg) continue;
|
|
14
69
|
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
15
70
|
if (arg === `--${name}` && i + 1 < argv.length) return argv[i + 1];
|
|
16
71
|
}
|
|
@@ -43,10 +98,25 @@ async function aivPost(path, body) {
|
|
|
43
98
|
});
|
|
44
99
|
if (!res.ok) {
|
|
45
100
|
const text = await res.text();
|
|
46
|
-
throw new Error(
|
|
101
|
+
throw new Error(errorMessageForMcp(res.status, path, text));
|
|
47
102
|
}
|
|
48
|
-
return res.json();
|
|
103
|
+
return toCite42McpResult(await res.json());
|
|
49
104
|
}
|
|
105
|
+
async function aivGet(path) {
|
|
106
|
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
107
|
+
const res = await fetch(url, {
|
|
108
|
+
method: "GET",
|
|
109
|
+
headers: { Authorization: `Bearer ${getApiKey()}` }
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const text = await res.text();
|
|
113
|
+
throw new Error(errorMessageForMcp(res.status, path, text));
|
|
114
|
+
}
|
|
115
|
+
return toCite42McpResult(await res.json());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/server.ts
|
|
119
|
+
var import_mcp = require("@mastra/mcp");
|
|
50
120
|
|
|
51
121
|
// src/tools.ts
|
|
52
122
|
var import_tools = require("@mastra/core/tools");
|
|
@@ -56,9 +126,16 @@ var QUERY_MAX_CHARS = 2e3;
|
|
|
56
126
|
var SHORT_TEXT_MAX_CHARS = 200;
|
|
57
127
|
var TIMEFRAME_MAX_CHARS = 80;
|
|
58
128
|
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.';
|
|
59
|
-
var MCP_USAGE_GUIDANCE = "
|
|
60
|
-
var MODEL_PRICING = "Pricing
|
|
61
|
-
var DATA_PRICING = "
|
|
129
|
+
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.";
|
|
130
|
+
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.";
|
|
131
|
+
var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
|
|
132
|
+
var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the primitive endpoints that run successfully and the selected model rates.";
|
|
133
|
+
function workflowDescription(description) {
|
|
134
|
+
return `${description} ${MCP_USAGE_GUIDANCE} ${MODEL_PRICING} ${DATA_PRICING} ${WORKFLOW_PRICING}`;
|
|
135
|
+
}
|
|
136
|
+
function readOnly(title, openWorldHint = true) {
|
|
137
|
+
return { annotations: { title, readOnlyHint: true, openWorldHint } };
|
|
138
|
+
}
|
|
62
139
|
function queryText(description) {
|
|
63
140
|
return import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
|
|
64
141
|
}
|
|
@@ -70,8 +147,52 @@ function optionalShortText(description) {
|
|
|
70
147
|
}
|
|
71
148
|
var modelsInput = import_zod.z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
|
|
72
149
|
var competitorList = import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
|
|
150
|
+
var aivCredits = (0, import_tools.createTool)({
|
|
151
|
+
id: "cite42_credits",
|
|
152
|
+
mcp: readOnly("Check Cite42 credit balance", false),
|
|
153
|
+
description: "Check the remaining Cite42 credit balance for the connected account. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
|
|
154
|
+
inputSchema: import_zod.z.object({}),
|
|
155
|
+
execute: async () => {
|
|
156
|
+
const usage = await aivGet("/usage");
|
|
157
|
+
const microCredits = usage.balance?.microCredits;
|
|
158
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
159
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
160
|
+
}
|
|
161
|
+
return toCite42CreditsBalance(microCredits);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
var aivPricing = (0, import_tools.createTool)({
|
|
165
|
+
id: "cite42_pricing",
|
|
166
|
+
mcp: readOnly("Get Cite42 price table", false),
|
|
167
|
+
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,
|
|
168
|
+
inputSchema: import_zod.z.object({}),
|
|
169
|
+
execute: async () => aivGet("/pricing")
|
|
170
|
+
});
|
|
171
|
+
var aivUsage = (0, import_tools.createTool)({
|
|
172
|
+
id: "cite42_usage",
|
|
173
|
+
mcp: readOnly("Get recent Cite42 usage", false),
|
|
174
|
+
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,
|
|
175
|
+
inputSchema: import_zod.z.object({}),
|
|
176
|
+
execute: async () => {
|
|
177
|
+
const usage = await aivGet("/usage");
|
|
178
|
+
const microCredits = usage.balance?.microCredits;
|
|
179
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
180
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
...toCite42CreditsBalance(microCredits),
|
|
184
|
+
recentCalls: (usage.recent ?? []).slice(0, 20).map((row) => ({
|
|
185
|
+
endpoint: row.endpoint,
|
|
186
|
+
status: row.status,
|
|
187
|
+
credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
|
|
188
|
+
at: row.createdAt
|
|
189
|
+
}))
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
});
|
|
73
193
|
var aivSearch = (0, import_tools.createTool)({
|
|
74
194
|
id: "cite42_search",
|
|
195
|
+
mcp: readOnly("Search AI models"),
|
|
75
196
|
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}`,
|
|
76
197
|
inputSchema: import_zod.z.object({
|
|
77
198
|
query: queryText("The concise AI-search/market-research query to run against the selected models."),
|
|
@@ -81,6 +202,7 @@ var aivSearch = (0, import_tools.createTool)({
|
|
|
81
202
|
});
|
|
82
203
|
var aivCitations = (0, import_tools.createTool)({
|
|
83
204
|
id: "cite42_citations",
|
|
205
|
+
mcp: readOnly("Find AI citations"),
|
|
84
206
|
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}`,
|
|
85
207
|
inputSchema: import_zod.z.object({
|
|
86
208
|
query: queryText("The concise query to look up cited sources for."),
|
|
@@ -91,6 +213,7 @@ var aivCitations = (0, import_tools.createTool)({
|
|
|
91
213
|
});
|
|
92
214
|
var aivRankings = (0, import_tools.createTool)({
|
|
93
215
|
id: "cite42_rankings",
|
|
216
|
+
mcp: readOnly("Measure AI brand rankings"),
|
|
94
217
|
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}`,
|
|
95
218
|
inputSchema: import_zod.z.object({
|
|
96
219
|
query: queryText("The concise query to measure brand/competitor visibility on."),
|
|
@@ -101,6 +224,7 @@ var aivRankings = (0, import_tools.createTool)({
|
|
|
101
224
|
});
|
|
102
225
|
var aivCompare = (0, import_tools.createTool)({
|
|
103
226
|
id: "cite42_compare",
|
|
227
|
+
mcp: readOnly("Compare brand against competitors"),
|
|
104
228
|
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}`,
|
|
105
229
|
inputSchema: import_zod.z.object({
|
|
106
230
|
query: queryText("The concise query to compare the brand and competitors on."),
|
|
@@ -112,6 +236,7 @@ var aivCompare = (0, import_tools.createTool)({
|
|
|
112
236
|
});
|
|
113
237
|
var aivSentiment = (0, import_tools.createTool)({
|
|
114
238
|
id: "cite42_sentiment",
|
|
239
|
+
mcp: readOnly("Score brand sentiment in AI answers"),
|
|
115
240
|
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}`,
|
|
116
241
|
inputSchema: import_zod.z.object({
|
|
117
242
|
query: queryText("The concise query whose answers are scored for brand sentiment."),
|
|
@@ -122,26 +247,31 @@ var aivSentiment = (0, import_tools.createTool)({
|
|
|
122
247
|
});
|
|
123
248
|
var aivKeywords = (0, import_tools.createTool)({
|
|
124
249
|
id: "cite42_keywords",
|
|
125
|
-
|
|
250
|
+
mcp: readOnly("Get SEO keyword data"),
|
|
251
|
+
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}`,
|
|
126
252
|
inputSchema: import_zod.z.object({
|
|
127
253
|
seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
|
|
128
254
|
country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
|
|
129
|
-
ideas: import_zod.z.boolean().optional().describe(
|
|
255
|
+
ideas: import_zod.z.boolean().optional().describe(
|
|
256
|
+
"Omit for volume, CPC, and competition on the seed only. Set true only for related, alternative, or long-tail keyword ideas."
|
|
257
|
+
)
|
|
130
258
|
}),
|
|
131
259
|
execute: async (input) => aivPost("/keywords", input)
|
|
132
260
|
});
|
|
133
261
|
var aivTrends = (0, import_tools.createTool)({
|
|
134
262
|
id: "cite42_trends",
|
|
263
|
+
mcp: readOnly("Get Google Trends data"),
|
|
135
264
|
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}`,
|
|
136
265
|
inputSchema: import_zod.z.object({
|
|
137
266
|
term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
|
|
138
267
|
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".'),
|
|
139
|
-
geo: import_zod.z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United
|
|
268
|
+
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.')
|
|
140
269
|
}),
|
|
141
270
|
execute: async (input) => aivPost("/trends", input)
|
|
142
271
|
});
|
|
143
272
|
var aivRedditTrends = (0, import_tools.createTool)({
|
|
144
273
|
id: "cite42_reddit_trends",
|
|
274
|
+
mcp: readOnly("Get Reddit trend data"),
|
|
145
275
|
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}`,
|
|
146
276
|
inputSchema: import_zod.z.object({
|
|
147
277
|
topic: queryText("Topic, category, product, or audience to research on Reddit."),
|
|
@@ -152,6 +282,7 @@ var aivRedditTrends = (0, import_tools.createTool)({
|
|
|
152
282
|
});
|
|
153
283
|
var aivYoutubeTrends = (0, import_tools.createTool)({
|
|
154
284
|
id: "cite42_youtube_trends",
|
|
285
|
+
mcp: readOnly("Get YouTube trend data"),
|
|
155
286
|
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}`,
|
|
156
287
|
inputSchema: import_zod.z.object({
|
|
157
288
|
topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
|
|
@@ -163,7 +294,10 @@ var aivYoutubeTrends = (0, import_tools.createTool)({
|
|
|
163
294
|
var channelEnum = import_zod.z.enum(["ai_search", "seo_keywords", "google_trends", "reddit", "youtube"]);
|
|
164
295
|
var aivFindContentOpportunities = (0, import_tools.createTool)({
|
|
165
296
|
id: "cite42_find_content_opportunities",
|
|
166
|
-
|
|
297
|
+
mcp: readOnly("Find content opportunities"),
|
|
298
|
+
description: workflowDescription(
|
|
299
|
+
"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."
|
|
300
|
+
),
|
|
167
301
|
inputSchema: import_zod.z.object({
|
|
168
302
|
topic: queryText("Market, niche, product category, or audience to research."),
|
|
169
303
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -176,7 +310,10 @@ var aivFindContentOpportunities = (0, import_tools.createTool)({
|
|
|
176
310
|
});
|
|
177
311
|
var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
|
|
178
312
|
id: "cite42_analyze_topic_demand",
|
|
179
|
-
|
|
313
|
+
mcp: readOnly("Analyze topic demand"),
|
|
314
|
+
description: workflowDescription(
|
|
315
|
+
"Run the topic-demand workflow: SEO keyword data, Google Trends, AI answers, and audience conversations, aggregated in one response."
|
|
316
|
+
),
|
|
180
317
|
inputSchema: import_zod.z.object({
|
|
181
318
|
topic: queryText("Topic, keyword, category, or audience demand to analyze."),
|
|
182
319
|
country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
|
|
@@ -186,7 +323,10 @@ var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
|
|
|
186
323
|
});
|
|
187
324
|
var aivMapAiPrompts = (0, import_tools.createTool)({
|
|
188
325
|
id: "cite42_map_ai_prompts",
|
|
189
|
-
|
|
326
|
+
mcp: readOnly("Map buyer prompts for AI search"),
|
|
327
|
+
description: workflowDescription(
|
|
328
|
+
"Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data."
|
|
329
|
+
),
|
|
190
330
|
inputSchema: import_zod.z.object({
|
|
191
331
|
topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
|
|
192
332
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -199,7 +339,10 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
|
|
|
199
339
|
});
|
|
200
340
|
var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
|
|
201
341
|
id: "cite42_analyze_competitor_content",
|
|
202
|
-
|
|
342
|
+
mcp: readOnly("Analyze competitor content"),
|
|
343
|
+
description: workflowDescription(
|
|
344
|
+
"Run the competitor-content workflow: AI rankings, head-to-head comparison, citations, keyword demand, and topic coverage, aggregated in one response."
|
|
345
|
+
),
|
|
203
346
|
inputSchema: import_zod.z.object({
|
|
204
347
|
competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
|
|
205
348
|
topic: import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
|
|
@@ -210,7 +353,10 @@ var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
|
|
|
210
353
|
});
|
|
211
354
|
var aivFindAiCitationGaps = (0, import_tools.createTool)({
|
|
212
355
|
id: "cite42_find_ai_citation_gaps",
|
|
213
|
-
|
|
356
|
+
mcp: readOnly("Find AI citation gaps"),
|
|
357
|
+
description: workflowDescription(
|
|
358
|
+
"Run the AI-citation-gaps workflow: find queries where AI answers cite others but not your brand, domain, or URL, with the supporting data."
|
|
359
|
+
),
|
|
214
360
|
inputSchema: import_zod.z.object({
|
|
215
361
|
query: queryText("AI-search query to inspect for citation gaps."),
|
|
216
362
|
brand: shortText("Brand or domain that should earn citations."),
|
|
@@ -222,7 +368,10 @@ var aivFindAiCitationGaps = (0, import_tools.createTool)({
|
|
|
222
368
|
});
|
|
223
369
|
var aivGenerateContentBrief = (0, import_tools.createTool)({
|
|
224
370
|
id: "cite42_generate_content_brief",
|
|
225
|
-
|
|
371
|
+
mcp: readOnly("Generate content brief data"),
|
|
372
|
+
description: workflowDescription(
|
|
373
|
+
"Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data."
|
|
374
|
+
),
|
|
226
375
|
inputSchema: import_zod.z.object({
|
|
227
376
|
topic: queryText("Topic, keyword, or content idea to brief."),
|
|
228
377
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -233,6 +382,37 @@ var aivGenerateContentBrief = (0, import_tools.createTool)({
|
|
|
233
382
|
execute: async (input) => aivPost("/workflows/content-brief", input)
|
|
234
383
|
});
|
|
235
384
|
|
|
385
|
+
// src/server.ts
|
|
386
|
+
var CITE42_TOOLS = {
|
|
387
|
+
cite42_credits: aivCredits,
|
|
388
|
+
cite42_pricing: aivPricing,
|
|
389
|
+
cite42_usage: aivUsage,
|
|
390
|
+
cite42_search: aivSearch,
|
|
391
|
+
cite42_rankings: aivRankings,
|
|
392
|
+
cite42_compare: aivCompare,
|
|
393
|
+
cite42_citations: aivCitations,
|
|
394
|
+
cite42_sentiment: aivSentiment,
|
|
395
|
+
cite42_keywords: aivKeywords,
|
|
396
|
+
cite42_trends: aivTrends,
|
|
397
|
+
cite42_reddit_trends: aivRedditTrends,
|
|
398
|
+
cite42_youtube_trends: aivYoutubeTrends,
|
|
399
|
+
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
400
|
+
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
401
|
+
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
402
|
+
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
403
|
+
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
404
|
+
cite42_generate_content_brief: aivGenerateContentBrief
|
|
405
|
+
};
|
|
406
|
+
function buildCite42Server(version) {
|
|
407
|
+
return new import_mcp.MCPServer({
|
|
408
|
+
id: "cite42",
|
|
409
|
+
name: "Cite42",
|
|
410
|
+
version,
|
|
411
|
+
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
412
|
+
tools: CITE42_TOOLS
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
236
416
|
// src/stdio.ts
|
|
237
417
|
if (!resolveApiKey()) {
|
|
238
418
|
console.error(
|
|
@@ -240,30 +420,7 @@ if (!resolveApiKey()) {
|
|
|
240
420
|
);
|
|
241
421
|
process.exit(1);
|
|
242
422
|
}
|
|
243
|
-
|
|
244
|
-
id: "cite42",
|
|
245
|
-
name: "Cite42",
|
|
246
|
-
version: "0.1.14",
|
|
247
|
-
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
248
|
-
tools: {
|
|
249
|
-
cite42_search: aivSearch,
|
|
250
|
-
cite42_rankings: aivRankings,
|
|
251
|
-
cite42_compare: aivCompare,
|
|
252
|
-
cite42_citations: aivCitations,
|
|
253
|
-
cite42_sentiment: aivSentiment,
|
|
254
|
-
cite42_keywords: aivKeywords,
|
|
255
|
-
cite42_trends: aivTrends,
|
|
256
|
-
cite42_reddit_trends: aivRedditTrends,
|
|
257
|
-
cite42_youtube_trends: aivYoutubeTrends,
|
|
258
|
-
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
259
|
-
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
260
|
-
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
261
|
-
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
262
|
-
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
263
|
-
cite42_generate_content_brief: aivGenerateContentBrief
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
server.startStdio().catch((error) => {
|
|
423
|
+
buildCite42Server("0.1.16").startStdio().catch((error) => {
|
|
267
424
|
console.error("[@cite42/mcp] Fatal error:", error);
|
|
268
425
|
process.exit(1);
|
|
269
426
|
});
|
package/dist/stdio.mjs
CHANGED
|
@@ -1,15 +1,70 @@
|
|
|
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";
|
|
5
|
+
var MICRO_CREDITS_PER_CREDIT = 1e3;
|
|
6
|
+
function isJsonObject(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function toCredits(microCredits) {
|
|
10
|
+
return microCredits / MICRO_CREDITS_PER_CREDIT;
|
|
11
|
+
}
|
|
12
|
+
function formatCredits(microCredits) {
|
|
13
|
+
const credits = toCredits(microCredits);
|
|
14
|
+
return credits.toLocaleString(
|
|
15
|
+
"en-US",
|
|
16
|
+
Number.isInteger(credits) ? { maximumFractionDigits: 0 } : { minimumFractionDigits: 0, maximumFractionDigits: 2 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function toCite42McpResult(value) {
|
|
20
|
+
if (!isJsonObject(value)) return value;
|
|
21
|
+
const { billing, warning, ...result } = value;
|
|
22
|
+
if (!isJsonObject(billing)) return value;
|
|
23
|
+
const cost = isJsonObject(billing.cost) ? billing.cost.microCredits : void 0;
|
|
24
|
+
const balance = isJsonObject(billing.balance) ? billing.balance.microCredits : void 0;
|
|
25
|
+
if (typeof cost !== "number" || !Number.isFinite(cost) || typeof balance !== "number" || !Number.isFinite(balance)) {
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
const lowCredits = typeof warning === "string";
|
|
29
|
+
const message = `Cite42 credits: ${formatCredits(cost)} charged; ${formatCredits(balance)} remaining.`;
|
|
30
|
+
return {
|
|
31
|
+
...result,
|
|
32
|
+
cite42Credits: {
|
|
33
|
+
charged: toCredits(cost),
|
|
34
|
+
remaining: toCredits(balance),
|
|
35
|
+
message: lowCredits ? `${message} Low Cite42 credit balance.` : message
|
|
36
|
+
}
|
|
37
|
+
};
|
|
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
|
+
}
|
|
47
|
+
function errorMessageForMcp(status, path, body) {
|
|
48
|
+
try {
|
|
49
|
+
const payload = JSON.parse(body);
|
|
50
|
+
if (isJsonObject(payload) && isJsonObject(payload.error) && typeof payload.error.code === "string") {
|
|
51
|
+
if (payload.error.code === "daily_limit_exceeded" || payload.error.code === "monthly_limit_exceeded") {
|
|
52
|
+
return "Cite42 credit limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
|
|
53
|
+
}
|
|
54
|
+
if (payload.error.code === "insufficient_credits") {
|
|
55
|
+
return "Insufficient Cite42 credits. Top up at https://www.cite42.dev/app/billing.";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
return `Cite42 API ${status} ${path}: ${body}`;
|
|
61
|
+
}
|
|
8
62
|
function readFlag(name) {
|
|
9
63
|
const argv = process.argv.slice(2);
|
|
10
64
|
const prefix = `--${name}=`;
|
|
11
65
|
for (let i = 0; i < argv.length; i++) {
|
|
12
66
|
const arg = argv[i];
|
|
67
|
+
if (!arg) continue;
|
|
13
68
|
if (arg.startsWith(prefix)) return arg.slice(prefix.length);
|
|
14
69
|
if (arg === `--${name}` && i + 1 < argv.length) return argv[i + 1];
|
|
15
70
|
}
|
|
@@ -42,10 +97,25 @@ async function aivPost(path, body) {
|
|
|
42
97
|
});
|
|
43
98
|
if (!res.ok) {
|
|
44
99
|
const text = await res.text();
|
|
45
|
-
throw new Error(
|
|
100
|
+
throw new Error(errorMessageForMcp(res.status, path, text));
|
|
46
101
|
}
|
|
47
|
-
return res.json();
|
|
102
|
+
return toCite42McpResult(await res.json());
|
|
48
103
|
}
|
|
104
|
+
async function aivGet(path) {
|
|
105
|
+
const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
|
|
106
|
+
const res = await fetch(url, {
|
|
107
|
+
method: "GET",
|
|
108
|
+
headers: { Authorization: `Bearer ${getApiKey()}` }
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const text = await res.text();
|
|
112
|
+
throw new Error(errorMessageForMcp(res.status, path, text));
|
|
113
|
+
}
|
|
114
|
+
return toCite42McpResult(await res.json());
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/server.ts
|
|
118
|
+
import { MCPServer } from "@mastra/mcp";
|
|
49
119
|
|
|
50
120
|
// src/tools.ts
|
|
51
121
|
import { createTool } from "@mastra/core/tools";
|
|
@@ -55,9 +125,16 @@ var QUERY_MAX_CHARS = 2e3;
|
|
|
55
125
|
var SHORT_TEXT_MAX_CHARS = 200;
|
|
56
126
|
var TIMEFRAME_MAX_CHARS = 80;
|
|
57
127
|
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.';
|
|
58
|
-
var MCP_USAGE_GUIDANCE = "
|
|
59
|
-
var MODEL_PRICING = "Pricing
|
|
60
|
-
var DATA_PRICING = "
|
|
128
|
+
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.";
|
|
129
|
+
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.";
|
|
130
|
+
var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
|
|
131
|
+
var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the primitive endpoints that run successfully and the selected model rates.";
|
|
132
|
+
function workflowDescription(description) {
|
|
133
|
+
return `${description} ${MCP_USAGE_GUIDANCE} ${MODEL_PRICING} ${DATA_PRICING} ${WORKFLOW_PRICING}`;
|
|
134
|
+
}
|
|
135
|
+
function readOnly(title, openWorldHint = true) {
|
|
136
|
+
return { annotations: { title, readOnlyHint: true, openWorldHint } };
|
|
137
|
+
}
|
|
61
138
|
function queryText(description) {
|
|
62
139
|
return z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
|
|
63
140
|
}
|
|
@@ -69,8 +146,52 @@ function optionalShortText(description) {
|
|
|
69
146
|
}
|
|
70
147
|
var modelsInput = z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
|
|
71
148
|
var competitorList = z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
|
|
149
|
+
var aivCredits = createTool({
|
|
150
|
+
id: "cite42_credits",
|
|
151
|
+
mcp: readOnly("Check Cite42 credit balance", false),
|
|
152
|
+
description: "Check the remaining Cite42 credit balance for the connected account. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
|
|
153
|
+
inputSchema: z.object({}),
|
|
154
|
+
execute: async () => {
|
|
155
|
+
const usage = await aivGet("/usage");
|
|
156
|
+
const microCredits = usage.balance?.microCredits;
|
|
157
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
158
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
159
|
+
}
|
|
160
|
+
return toCite42CreditsBalance(microCredits);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
var aivPricing = createTool({
|
|
164
|
+
id: "cite42_pricing",
|
|
165
|
+
mcp: readOnly("Get Cite42 price table", false),
|
|
166
|
+
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,
|
|
167
|
+
inputSchema: z.object({}),
|
|
168
|
+
execute: async () => aivGet("/pricing")
|
|
169
|
+
});
|
|
170
|
+
var aivUsage = createTool({
|
|
171
|
+
id: "cite42_usage",
|
|
172
|
+
mcp: readOnly("Get recent Cite42 usage", false),
|
|
173
|
+
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,
|
|
174
|
+
inputSchema: z.object({}),
|
|
175
|
+
execute: async () => {
|
|
176
|
+
const usage = await aivGet("/usage");
|
|
177
|
+
const microCredits = usage.balance?.microCredits;
|
|
178
|
+
if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
|
|
179
|
+
throw new Error("Cite42 usage response did not include a balance.");
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
...toCite42CreditsBalance(microCredits),
|
|
183
|
+
recentCalls: (usage.recent ?? []).slice(0, 20).map((row) => ({
|
|
184
|
+
endpoint: row.endpoint,
|
|
185
|
+
status: row.status,
|
|
186
|
+
credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
|
|
187
|
+
at: row.createdAt
|
|
188
|
+
}))
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
72
192
|
var aivSearch = createTool({
|
|
73
193
|
id: "cite42_search",
|
|
194
|
+
mcp: readOnly("Search AI models"),
|
|
74
195
|
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}`,
|
|
75
196
|
inputSchema: z.object({
|
|
76
197
|
query: queryText("The concise AI-search/market-research query to run against the selected models."),
|
|
@@ -80,6 +201,7 @@ var aivSearch = createTool({
|
|
|
80
201
|
});
|
|
81
202
|
var aivCitations = createTool({
|
|
82
203
|
id: "cite42_citations",
|
|
204
|
+
mcp: readOnly("Find AI citations"),
|
|
83
205
|
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}`,
|
|
84
206
|
inputSchema: z.object({
|
|
85
207
|
query: queryText("The concise query to look up cited sources for."),
|
|
@@ -90,6 +212,7 @@ var aivCitations = createTool({
|
|
|
90
212
|
});
|
|
91
213
|
var aivRankings = createTool({
|
|
92
214
|
id: "cite42_rankings",
|
|
215
|
+
mcp: readOnly("Measure AI brand rankings"),
|
|
93
216
|
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}`,
|
|
94
217
|
inputSchema: z.object({
|
|
95
218
|
query: queryText("The concise query to measure brand/competitor visibility on."),
|
|
@@ -100,6 +223,7 @@ var aivRankings = createTool({
|
|
|
100
223
|
});
|
|
101
224
|
var aivCompare = createTool({
|
|
102
225
|
id: "cite42_compare",
|
|
226
|
+
mcp: readOnly("Compare brand against competitors"),
|
|
103
227
|
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}`,
|
|
104
228
|
inputSchema: z.object({
|
|
105
229
|
query: queryText("The concise query to compare the brand and competitors on."),
|
|
@@ -111,6 +235,7 @@ var aivCompare = createTool({
|
|
|
111
235
|
});
|
|
112
236
|
var aivSentiment = createTool({
|
|
113
237
|
id: "cite42_sentiment",
|
|
238
|
+
mcp: readOnly("Score brand sentiment in AI answers"),
|
|
114
239
|
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}`,
|
|
115
240
|
inputSchema: z.object({
|
|
116
241
|
query: queryText("The concise query whose answers are scored for brand sentiment."),
|
|
@@ -121,26 +246,31 @@ var aivSentiment = createTool({
|
|
|
121
246
|
});
|
|
122
247
|
var aivKeywords = createTool({
|
|
123
248
|
id: "cite42_keywords",
|
|
124
|
-
|
|
249
|
+
mcp: readOnly("Get SEO keyword data"),
|
|
250
|
+
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}`,
|
|
125
251
|
inputSchema: z.object({
|
|
126
252
|
seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
|
|
127
253
|
country: z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
|
|
128
|
-
ideas: z.boolean().optional().describe(
|
|
254
|
+
ideas: z.boolean().optional().describe(
|
|
255
|
+
"Omit for volume, CPC, and competition on the seed only. Set true only for related, alternative, or long-tail keyword ideas."
|
|
256
|
+
)
|
|
129
257
|
}),
|
|
130
258
|
execute: async (input) => aivPost("/keywords", input)
|
|
131
259
|
});
|
|
132
260
|
var aivTrends = createTool({
|
|
133
261
|
id: "cite42_trends",
|
|
262
|
+
mcp: readOnly("Get Google Trends data"),
|
|
134
263
|
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}`,
|
|
135
264
|
inputSchema: z.object({
|
|
136
265
|
term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
|
|
137
266
|
timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trends window, e.g. "past_12_months", "past_30_days", "past_5_years".'),
|
|
138
|
-
geo: z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United
|
|
267
|
+
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.')
|
|
139
268
|
}),
|
|
140
269
|
execute: async (input) => aivPost("/trends", input)
|
|
141
270
|
});
|
|
142
271
|
var aivRedditTrends = createTool({
|
|
143
272
|
id: "cite42_reddit_trends",
|
|
273
|
+
mcp: readOnly("Get Reddit trend data"),
|
|
144
274
|
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}`,
|
|
145
275
|
inputSchema: z.object({
|
|
146
276
|
topic: queryText("Topic, category, product, or audience to research on Reddit."),
|
|
@@ -151,6 +281,7 @@ var aivRedditTrends = createTool({
|
|
|
151
281
|
});
|
|
152
282
|
var aivYoutubeTrends = createTool({
|
|
153
283
|
id: "cite42_youtube_trends",
|
|
284
|
+
mcp: readOnly("Get YouTube trend data"),
|
|
154
285
|
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}`,
|
|
155
286
|
inputSchema: z.object({
|
|
156
287
|
topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
|
|
@@ -162,7 +293,10 @@ var aivYoutubeTrends = createTool({
|
|
|
162
293
|
var channelEnum = z.enum(["ai_search", "seo_keywords", "google_trends", "reddit", "youtube"]);
|
|
163
294
|
var aivFindContentOpportunities = createTool({
|
|
164
295
|
id: "cite42_find_content_opportunities",
|
|
165
|
-
|
|
296
|
+
mcp: readOnly("Find content opportunities"),
|
|
297
|
+
description: workflowDescription(
|
|
298
|
+
"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."
|
|
299
|
+
),
|
|
166
300
|
inputSchema: z.object({
|
|
167
301
|
topic: queryText("Market, niche, product category, or audience to research."),
|
|
168
302
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -175,7 +309,10 @@ var aivFindContentOpportunities = createTool({
|
|
|
175
309
|
});
|
|
176
310
|
var aivAnalyzeTopicDemand = createTool({
|
|
177
311
|
id: "cite42_analyze_topic_demand",
|
|
178
|
-
|
|
312
|
+
mcp: readOnly("Analyze topic demand"),
|
|
313
|
+
description: workflowDescription(
|
|
314
|
+
"Run the topic-demand workflow: SEO keyword data, Google Trends, AI answers, and audience conversations, aggregated in one response."
|
|
315
|
+
),
|
|
179
316
|
inputSchema: z.object({
|
|
180
317
|
topic: queryText("Topic, keyword, category, or audience demand to analyze."),
|
|
181
318
|
country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
|
|
@@ -185,7 +322,10 @@ var aivAnalyzeTopicDemand = createTool({
|
|
|
185
322
|
});
|
|
186
323
|
var aivMapAiPrompts = createTool({
|
|
187
324
|
id: "cite42_map_ai_prompts",
|
|
188
|
-
|
|
325
|
+
mcp: readOnly("Map buyer prompts for AI search"),
|
|
326
|
+
description: workflowDescription(
|
|
327
|
+
"Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data."
|
|
328
|
+
),
|
|
189
329
|
inputSchema: z.object({
|
|
190
330
|
topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
|
|
191
331
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -198,7 +338,10 @@ var aivMapAiPrompts = createTool({
|
|
|
198
338
|
});
|
|
199
339
|
var aivAnalyzeCompetitorContent = createTool({
|
|
200
340
|
id: "cite42_analyze_competitor_content",
|
|
201
|
-
|
|
341
|
+
mcp: readOnly("Analyze competitor content"),
|
|
342
|
+
description: workflowDescription(
|
|
343
|
+
"Run the competitor-content workflow: AI rankings, head-to-head comparison, citations, keyword demand, and topic coverage, aggregated in one response."
|
|
344
|
+
),
|
|
202
345
|
inputSchema: z.object({
|
|
203
346
|
competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
|
|
204
347
|
topic: z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
|
|
@@ -209,7 +352,10 @@ var aivAnalyzeCompetitorContent = createTool({
|
|
|
209
352
|
});
|
|
210
353
|
var aivFindAiCitationGaps = createTool({
|
|
211
354
|
id: "cite42_find_ai_citation_gaps",
|
|
212
|
-
|
|
355
|
+
mcp: readOnly("Find AI citation gaps"),
|
|
356
|
+
description: workflowDescription(
|
|
357
|
+
"Run the AI-citation-gaps workflow: find queries where AI answers cite others but not your brand, domain, or URL, with the supporting data."
|
|
358
|
+
),
|
|
213
359
|
inputSchema: z.object({
|
|
214
360
|
query: queryText("AI-search query to inspect for citation gaps."),
|
|
215
361
|
brand: shortText("Brand or domain that should earn citations."),
|
|
@@ -221,7 +367,10 @@ var aivFindAiCitationGaps = createTool({
|
|
|
221
367
|
});
|
|
222
368
|
var aivGenerateContentBrief = createTool({
|
|
223
369
|
id: "cite42_generate_content_brief",
|
|
224
|
-
|
|
370
|
+
mcp: readOnly("Generate content brief data"),
|
|
371
|
+
description: workflowDescription(
|
|
372
|
+
"Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data."
|
|
373
|
+
),
|
|
225
374
|
inputSchema: z.object({
|
|
226
375
|
topic: queryText("Topic, keyword, or content idea to brief."),
|
|
227
376
|
audience: optionalShortText("Optional target audience or ICP."),
|
|
@@ -232,6 +381,37 @@ var aivGenerateContentBrief = createTool({
|
|
|
232
381
|
execute: async (input) => aivPost("/workflows/content-brief", input)
|
|
233
382
|
});
|
|
234
383
|
|
|
384
|
+
// src/server.ts
|
|
385
|
+
var CITE42_TOOLS = {
|
|
386
|
+
cite42_credits: aivCredits,
|
|
387
|
+
cite42_pricing: aivPricing,
|
|
388
|
+
cite42_usage: aivUsage,
|
|
389
|
+
cite42_search: aivSearch,
|
|
390
|
+
cite42_rankings: aivRankings,
|
|
391
|
+
cite42_compare: aivCompare,
|
|
392
|
+
cite42_citations: aivCitations,
|
|
393
|
+
cite42_sentiment: aivSentiment,
|
|
394
|
+
cite42_keywords: aivKeywords,
|
|
395
|
+
cite42_trends: aivTrends,
|
|
396
|
+
cite42_reddit_trends: aivRedditTrends,
|
|
397
|
+
cite42_youtube_trends: aivYoutubeTrends,
|
|
398
|
+
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
399
|
+
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
400
|
+
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
401
|
+
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
402
|
+
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
403
|
+
cite42_generate_content_brief: aivGenerateContentBrief
|
|
404
|
+
};
|
|
405
|
+
function buildCite42Server(version) {
|
|
406
|
+
return new MCPServer({
|
|
407
|
+
id: "cite42",
|
|
408
|
+
name: "Cite42",
|
|
409
|
+
version,
|
|
410
|
+
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
411
|
+
tools: CITE42_TOOLS
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
235
415
|
// src/stdio.ts
|
|
236
416
|
if (!resolveApiKey()) {
|
|
237
417
|
console.error(
|
|
@@ -239,30 +419,7 @@ if (!resolveApiKey()) {
|
|
|
239
419
|
);
|
|
240
420
|
process.exit(1);
|
|
241
421
|
}
|
|
242
|
-
|
|
243
|
-
id: "cite42",
|
|
244
|
-
name: "Cite42",
|
|
245
|
-
version: "0.1.14",
|
|
246
|
-
description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
|
|
247
|
-
tools: {
|
|
248
|
-
cite42_search: aivSearch,
|
|
249
|
-
cite42_rankings: aivRankings,
|
|
250
|
-
cite42_compare: aivCompare,
|
|
251
|
-
cite42_citations: aivCitations,
|
|
252
|
-
cite42_sentiment: aivSentiment,
|
|
253
|
-
cite42_keywords: aivKeywords,
|
|
254
|
-
cite42_trends: aivTrends,
|
|
255
|
-
cite42_reddit_trends: aivRedditTrends,
|
|
256
|
-
cite42_youtube_trends: aivYoutubeTrends,
|
|
257
|
-
cite42_find_content_opportunities: aivFindContentOpportunities,
|
|
258
|
-
cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
|
|
259
|
-
cite42_map_ai_prompts: aivMapAiPrompts,
|
|
260
|
-
cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
|
|
261
|
-
cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
|
|
262
|
-
cite42_generate_content_brief: aivGenerateContentBrief
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
server.startStdio().catch((error) => {
|
|
422
|
+
buildCite42Server("0.1.16").startStdio().catch((error) => {
|
|
266
423
|
console.error("[@cite42/mcp] Fatal error:", error);
|
|
267
424
|
process.exit(1);
|
|
268
425
|
});
|
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.16",
|
|
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"
|