@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 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 installed, your MCP client (Claude Code, Claude Desktop, Codex CLI, Cursor, Windsurf, etc.) can:
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
- - **Node.js 18 or newer** (`node --version` to check)
18
- - **A Cite42 API key**, free to create at [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys)
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
- There is nothing to install manually. Every client below runs the server on demand with `npx -y @cite42/mcp`. You only need to register it once.
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
- #### Claude Desktop
62
+ #### Cursor
47
63
 
48
- Add this to `claude_desktop_config.json` (**Settings → Developer → Edit Config**):
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 the config is only picked up on a fresh start:
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
- - **Claude Desktop / Cursor / Windsurf**: fully quit and reopen the app
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. The MCP client starts it automatically when the session needs tools.
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
- - **Claude Desktop / Cursor:** look for `cite42` in the MCP/tools indicator. 15 tools should be listed.
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, competition level, and related keyword ideas.
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(`Cite42 API ${res.status} ${path}: ${text}`);
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 = "Use Cite42 for AI-search, citation, SEO, keyword, trend, and market-research data. Do not use it as a general coding or writing assistant; code-related research is fine when the goal is to compare what AI models recommend or cite. If a plan needs more than 20 endpoint calls or may cost over $5, estimate the cost and ask the user before continuing.";
60
- var MODEL_PRICING = "Pricing: search/citations/rankings/compare are model-specific: Google AI Overview $0.01, Gemini $0.02, ChatGPT/Claude $0.04, Perplexity $0.05 per selected model. Sentiment: Google AI Overview $0.03, Gemini $0.04, ChatGPT/Claude $0.06, Perplexity $0.07. Omitting models queries all five AI surfaces. Use the models field when the user names a specific surface or asks to reduce cost.";
61
- var DATA_PRICING = "Pricing: keywords $0.15/call, trends $0.01/call, Reddit trends $0.05/call, YouTube trends $0.05/call.";
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
- description: `Classic keyword data for a seed term: monthly search volume, CPC, competition, and (optionally) LLM-curated related keyword ideas with volume, CPC, competition, and competition level from provider candidates. A flat-priced data tool, not a multi-model AI-search query. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
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("When true, also return LLM-curated related keyword ideas with volume, CPC, competition, and competition level.")
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 States"). Worldwide when omitted.')
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
- description: `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. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the topic-demand workflow: SEO keyword data, Google Trends, AI answers, and audience conversations, aggregated in one response. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the competitor-content workflow: AI rankings, head-to-head comparison, citations, keyword demand, and topic coverage, aggregated in one response. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the AI-citation-gaps workflow: find queries where AI answers cite others but not your brand, domain, or URL, with the supporting data. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data. ${MCP_USAGE_GUIDANCE}`,
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
- var server = new import_mcp.MCPServer({
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(`Cite42 API ${res.status} ${path}: ${text}`);
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 = "Use Cite42 for AI-search, citation, SEO, keyword, trend, and market-research data. Do not use it as a general coding or writing assistant; code-related research is fine when the goal is to compare what AI models recommend or cite. If a plan needs more than 20 endpoint calls or may cost over $5, estimate the cost and ask the user before continuing.";
59
- var MODEL_PRICING = "Pricing: search/citations/rankings/compare are model-specific: Google AI Overview $0.01, Gemini $0.02, ChatGPT/Claude $0.04, Perplexity $0.05 per selected model. Sentiment: Google AI Overview $0.03, Gemini $0.04, ChatGPT/Claude $0.06, Perplexity $0.07. Omitting models queries all five AI surfaces. Use the models field when the user names a specific surface or asks to reduce cost.";
60
- var DATA_PRICING = "Pricing: keywords $0.15/call, trends $0.01/call, Reddit trends $0.05/call, YouTube trends $0.05/call.";
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
- description: `Classic keyword data for a seed term: monthly search volume, CPC, competition, and (optionally) LLM-curated related keyword ideas with volume, CPC, competition, and competition level from provider candidates. A flat-priced data tool, not a multi-model AI-search query. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
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("When true, also return LLM-curated related keyword ideas with volume, CPC, competition, and competition level.")
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 States"). Worldwide when omitted.')
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
- description: `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. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the topic-demand workflow: SEO keyword data, Google Trends, AI answers, and audience conversations, aggregated in one response. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the competitor-content workflow: AI rankings, head-to-head comparison, citations, keyword demand, and topic coverage, aggregated in one response. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the AI-citation-gaps workflow: find queries where AI answers cite others but not your brand, domain, or URL, with the supporting data. ${MCP_USAGE_GUIDANCE}`,
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
- description: `Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data. ${MCP_USAGE_GUIDANCE}`,
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
- var server = new MCPServer({
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.14",
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"