@cite42/mcp 0.1.15 → 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,9 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- // src/stdio.ts
5
- var import_mcp = require("@mastra/mcp");
6
-
7
4
  // src/client.ts
8
5
  var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
9
6
  var MICRO_CREDITS_PER_CREDIT = 1e3;
@@ -40,6 +37,14 @@ function toCite42McpResult(value) {
40
37
  }
41
38
  };
42
39
  }
40
+ function toCite42CreditsBalance(balanceMicroCredits) {
41
+ return {
42
+ cite42Credits: {
43
+ remaining: toCredits(balanceMicroCredits),
44
+ message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
45
+ }
46
+ };
47
+ }
43
48
  function errorMessageForMcp(status, path, body) {
44
49
  try {
45
50
  const payload = JSON.parse(body);
@@ -60,6 +65,7 @@ function readFlag(name) {
60
65
  const prefix = `--${name}=`;
61
66
  for (let i = 0; i < argv.length; i++) {
62
67
  const arg = argv[i];
68
+ if (!arg) continue;
63
69
  if (arg.startsWith(prefix)) return arg.slice(prefix.length);
64
70
  if (arg === `--${name}` && i + 1 < argv.length) return argv[i + 1];
65
71
  }
@@ -96,6 +102,21 @@ async function aivPost(path, body) {
96
102
  }
97
103
  return toCite42McpResult(await res.json());
98
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");
99
120
 
100
121
  // src/tools.ts
101
122
  var import_tools = require("@mastra/core/tools");
@@ -105,9 +126,16 @@ var QUERY_MAX_CHARS = 2e3;
105
126
  var SHORT_TEXT_MAX_CHARS = 200;
106
127
  var TIMEFRAME_MAX_CHARS = 80;
107
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.';
108
- 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 use more than 500 Cite42 credits, estimate the credits and ask the user before continuing. When a result contains cite42Credits, use its message if reporting it: it is Cite42 credits, not a model or host balance.";
109
- var MODEL_PRICING = "Pricing in Cite42 credits: search/citations/rankings/compare are model-specific: Google AI Overview 1, Gemini 2, ChatGPT/Claude 4, Perplexity 5 per selected model. Sentiment: Google AI Overview 3, Gemini 4, ChatGPT/Claude 6, Perplexity 7. Omitting models queries all five AI surfaces. Use the models field when the user names a specific surface or asks to reduce credit use.";
110
- var DATA_PRICING = "Pricing in Cite42 credits: keywords 15/call, trends 1/call, Reddit trends 5/call, YouTube trends 5/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
+ }
111
139
  function queryText(description) {
112
140
  return import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
113
141
  }
@@ -119,8 +147,52 @@ function optionalShortText(description) {
119
147
  }
120
148
  var modelsInput = import_zod.z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
121
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
+ });
122
193
  var aivSearch = (0, import_tools.createTool)({
123
194
  id: "cite42_search",
195
+ mcp: readOnly("Search AI models"),
124
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}`,
125
197
  inputSchema: import_zod.z.object({
126
198
  query: queryText("The concise AI-search/market-research query to run against the selected models."),
@@ -130,6 +202,7 @@ var aivSearch = (0, import_tools.createTool)({
130
202
  });
131
203
  var aivCitations = (0, import_tools.createTool)({
132
204
  id: "cite42_citations",
205
+ mcp: readOnly("Find AI citations"),
133
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}`,
134
207
  inputSchema: import_zod.z.object({
135
208
  query: queryText("The concise query to look up cited sources for."),
@@ -140,6 +213,7 @@ var aivCitations = (0, import_tools.createTool)({
140
213
  });
141
214
  var aivRankings = (0, import_tools.createTool)({
142
215
  id: "cite42_rankings",
216
+ mcp: readOnly("Measure AI brand rankings"),
143
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}`,
144
218
  inputSchema: import_zod.z.object({
145
219
  query: queryText("The concise query to measure brand/competitor visibility on."),
@@ -150,6 +224,7 @@ var aivRankings = (0, import_tools.createTool)({
150
224
  });
151
225
  var aivCompare = (0, import_tools.createTool)({
152
226
  id: "cite42_compare",
227
+ mcp: readOnly("Compare brand against competitors"),
153
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}`,
154
229
  inputSchema: import_zod.z.object({
155
230
  query: queryText("The concise query to compare the brand and competitors on."),
@@ -161,6 +236,7 @@ var aivCompare = (0, import_tools.createTool)({
161
236
  });
162
237
  var aivSentiment = (0, import_tools.createTool)({
163
238
  id: "cite42_sentiment",
239
+ mcp: readOnly("Score brand sentiment in AI answers"),
164
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}`,
165
241
  inputSchema: import_zod.z.object({
166
242
  query: queryText("The concise query whose answers are scored for brand sentiment."),
@@ -171,26 +247,31 @@ var aivSentiment = (0, import_tools.createTool)({
171
247
  });
172
248
  var aivKeywords = (0, import_tools.createTool)({
173
249
  id: "cite42_keywords",
174
- 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}`,
175
252
  inputSchema: import_zod.z.object({
176
253
  seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
177
254
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
178
- ideas: import_zod.z.boolean().optional().describe("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
+ )
179
258
  }),
180
259
  execute: async (input) => aivPost("/keywords", input)
181
260
  });
182
261
  var aivTrends = (0, import_tools.createTool)({
183
262
  id: "cite42_trends",
263
+ mcp: readOnly("Get Google Trends data"),
184
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}`,
185
265
  inputSchema: import_zod.z.object({
186
266
  term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
187
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".'),
188
- 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.')
189
269
  }),
190
270
  execute: async (input) => aivPost("/trends", input)
191
271
  });
192
272
  var aivRedditTrends = (0, import_tools.createTool)({
193
273
  id: "cite42_reddit_trends",
274
+ mcp: readOnly("Get Reddit trend data"),
194
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}`,
195
276
  inputSchema: import_zod.z.object({
196
277
  topic: queryText("Topic, category, product, or audience to research on Reddit."),
@@ -201,6 +282,7 @@ var aivRedditTrends = (0, import_tools.createTool)({
201
282
  });
202
283
  var aivYoutubeTrends = (0, import_tools.createTool)({
203
284
  id: "cite42_youtube_trends",
285
+ mcp: readOnly("Get YouTube trend data"),
204
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}`,
205
287
  inputSchema: import_zod.z.object({
206
288
  topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
@@ -212,7 +294,10 @@ var aivYoutubeTrends = (0, import_tools.createTool)({
212
294
  var channelEnum = import_zod.z.enum(["ai_search", "seo_keywords", "google_trends", "reddit", "youtube"]);
213
295
  var aivFindContentOpportunities = (0, import_tools.createTool)({
214
296
  id: "cite42_find_content_opportunities",
215
- 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
+ ),
216
301
  inputSchema: import_zod.z.object({
217
302
  topic: queryText("Market, niche, product category, or audience to research."),
218
303
  audience: optionalShortText("Optional target audience or ICP."),
@@ -225,7 +310,10 @@ var aivFindContentOpportunities = (0, import_tools.createTool)({
225
310
  });
226
311
  var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
227
312
  id: "cite42_analyze_topic_demand",
228
- 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
+ ),
229
317
  inputSchema: import_zod.z.object({
230
318
  topic: queryText("Topic, keyword, category, or audience demand to analyze."),
231
319
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
@@ -235,7 +323,10 @@ var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
235
323
  });
236
324
  var aivMapAiPrompts = (0, import_tools.createTool)({
237
325
  id: "cite42_map_ai_prompts",
238
- 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
+ ),
239
330
  inputSchema: import_zod.z.object({
240
331
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
241
332
  audience: optionalShortText("Optional target audience or ICP."),
@@ -248,7 +339,10 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
248
339
  });
249
340
  var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
250
341
  id: "cite42_analyze_competitor_content",
251
- 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
+ ),
252
346
  inputSchema: import_zod.z.object({
253
347
  competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
254
348
  topic: import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
@@ -259,7 +353,10 @@ var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
259
353
  });
260
354
  var aivFindAiCitationGaps = (0, import_tools.createTool)({
261
355
  id: "cite42_find_ai_citation_gaps",
262
- 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
+ ),
263
360
  inputSchema: import_zod.z.object({
264
361
  query: queryText("AI-search query to inspect for citation gaps."),
265
362
  brand: shortText("Brand or domain that should earn citations."),
@@ -271,7 +368,10 @@ var aivFindAiCitationGaps = (0, import_tools.createTool)({
271
368
  });
272
369
  var aivGenerateContentBrief = (0, import_tools.createTool)({
273
370
  id: "cite42_generate_content_brief",
274
- 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
+ ),
275
375
  inputSchema: import_zod.z.object({
276
376
  topic: queryText("Topic, keyword, or content idea to brief."),
277
377
  audience: optionalShortText("Optional target audience or ICP."),
@@ -282,6 +382,37 @@ var aivGenerateContentBrief = (0, import_tools.createTool)({
282
382
  execute: async (input) => aivPost("/workflows/content-brief", input)
283
383
  });
284
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
+
285
416
  // src/stdio.ts
286
417
  if (!resolveApiKey()) {
287
418
  console.error(
@@ -289,30 +420,7 @@ if (!resolveApiKey()) {
289
420
  );
290
421
  process.exit(1);
291
422
  }
292
- var server = new import_mcp.MCPServer({
293
- id: "cite42",
294
- name: "Cite42",
295
- version: "0.1.15",
296
- description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
297
- tools: {
298
- cite42_search: aivSearch,
299
- cite42_rankings: aivRankings,
300
- cite42_compare: aivCompare,
301
- cite42_citations: aivCitations,
302
- cite42_sentiment: aivSentiment,
303
- cite42_keywords: aivKeywords,
304
- cite42_trends: aivTrends,
305
- cite42_reddit_trends: aivRedditTrends,
306
- cite42_youtube_trends: aivYoutubeTrends,
307
- cite42_find_content_opportunities: aivFindContentOpportunities,
308
- cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
309
- cite42_map_ai_prompts: aivMapAiPrompts,
310
- cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
311
- cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
312
- cite42_generate_content_brief: aivGenerateContentBrief
313
- }
314
- });
315
- server.startStdio().catch((error) => {
423
+ buildCite42Server("0.1.16").startStdio().catch((error) => {
316
424
  console.error("[@cite42/mcp] Fatal error:", error);
317
425
  process.exit(1);
318
426
  });
package/dist/stdio.mjs CHANGED
@@ -1,8 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/stdio.ts
4
- import { MCPServer } from "@mastra/mcp";
5
-
6
3
  // src/client.ts
7
4
  var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
8
5
  var MICRO_CREDITS_PER_CREDIT = 1e3;
@@ -39,6 +36,14 @@ function toCite42McpResult(value) {
39
36
  }
40
37
  };
41
38
  }
39
+ function toCite42CreditsBalance(balanceMicroCredits) {
40
+ return {
41
+ cite42Credits: {
42
+ remaining: toCredits(balanceMicroCredits),
43
+ message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
44
+ }
45
+ };
46
+ }
42
47
  function errorMessageForMcp(status, path, body) {
43
48
  try {
44
49
  const payload = JSON.parse(body);
@@ -59,6 +64,7 @@ function readFlag(name) {
59
64
  const prefix = `--${name}=`;
60
65
  for (let i = 0; i < argv.length; i++) {
61
66
  const arg = argv[i];
67
+ if (!arg) continue;
62
68
  if (arg.startsWith(prefix)) return arg.slice(prefix.length);
63
69
  if (arg === `--${name}` && i + 1 < argv.length) return argv[i + 1];
64
70
  }
@@ -95,6 +101,21 @@ async function aivPost(path, body) {
95
101
  }
96
102
  return toCite42McpResult(await res.json());
97
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";
98
119
 
99
120
  // src/tools.ts
100
121
  import { createTool } from "@mastra/core/tools";
@@ -104,9 +125,16 @@ var QUERY_MAX_CHARS = 2e3;
104
125
  var SHORT_TEXT_MAX_CHARS = 200;
105
126
  var TIMEFRAME_MAX_CHARS = 80;
106
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.';
107
- 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 use more than 500 Cite42 credits, estimate the credits and ask the user before continuing. When a result contains cite42Credits, use its message if reporting it: it is Cite42 credits, not a model or host balance.";
108
- var MODEL_PRICING = "Pricing in Cite42 credits: search/citations/rankings/compare are model-specific: Google AI Overview 1, Gemini 2, ChatGPT/Claude 4, Perplexity 5 per selected model. Sentiment: Google AI Overview 3, Gemini 4, ChatGPT/Claude 6, Perplexity 7. Omitting models queries all five AI surfaces. Use the models field when the user names a specific surface or asks to reduce credit use.";
109
- var DATA_PRICING = "Pricing in Cite42 credits: keywords 15/call, trends 1/call, Reddit trends 5/call, YouTube trends 5/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
+ }
110
138
  function queryText(description) {
111
139
  return z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
112
140
  }
@@ -118,8 +146,52 @@ function optionalShortText(description) {
118
146
  }
119
147
  var modelsInput = z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
120
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
+ });
121
192
  var aivSearch = createTool({
122
193
  id: "cite42_search",
194
+ mcp: readOnly("Search AI models"),
123
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}`,
124
196
  inputSchema: z.object({
125
197
  query: queryText("The concise AI-search/market-research query to run against the selected models."),
@@ -129,6 +201,7 @@ var aivSearch = createTool({
129
201
  });
130
202
  var aivCitations = createTool({
131
203
  id: "cite42_citations",
204
+ mcp: readOnly("Find AI citations"),
132
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}`,
133
206
  inputSchema: z.object({
134
207
  query: queryText("The concise query to look up cited sources for."),
@@ -139,6 +212,7 @@ var aivCitations = createTool({
139
212
  });
140
213
  var aivRankings = createTool({
141
214
  id: "cite42_rankings",
215
+ mcp: readOnly("Measure AI brand rankings"),
142
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}`,
143
217
  inputSchema: z.object({
144
218
  query: queryText("The concise query to measure brand/competitor visibility on."),
@@ -149,6 +223,7 @@ var aivRankings = createTool({
149
223
  });
150
224
  var aivCompare = createTool({
151
225
  id: "cite42_compare",
226
+ mcp: readOnly("Compare brand against competitors"),
152
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}`,
153
228
  inputSchema: z.object({
154
229
  query: queryText("The concise query to compare the brand and competitors on."),
@@ -160,6 +235,7 @@ var aivCompare = createTool({
160
235
  });
161
236
  var aivSentiment = createTool({
162
237
  id: "cite42_sentiment",
238
+ mcp: readOnly("Score brand sentiment in AI answers"),
163
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}`,
164
240
  inputSchema: z.object({
165
241
  query: queryText("The concise query whose answers are scored for brand sentiment."),
@@ -170,26 +246,31 @@ var aivSentiment = createTool({
170
246
  });
171
247
  var aivKeywords = createTool({
172
248
  id: "cite42_keywords",
173
- 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}`,
174
251
  inputSchema: z.object({
175
252
  seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
176
253
  country: z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
177
- 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
+ )
178
257
  }),
179
258
  execute: async (input) => aivPost("/keywords", input)
180
259
  });
181
260
  var aivTrends = createTool({
182
261
  id: "cite42_trends",
262
+ mcp: readOnly("Get Google Trends data"),
183
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}`,
184
264
  inputSchema: z.object({
185
265
  term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
186
266
  timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trends window, e.g. "past_12_months", "past_30_days", "past_5_years".'),
187
- geo: z.string().trim().max(SHORT_TEXT_MAX_CHARS).optional().describe('Optional location name (e.g. "United 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.')
188
268
  }),
189
269
  execute: async (input) => aivPost("/trends", input)
190
270
  });
191
271
  var aivRedditTrends = createTool({
192
272
  id: "cite42_reddit_trends",
273
+ mcp: readOnly("Get Reddit trend data"),
193
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}`,
194
275
  inputSchema: z.object({
195
276
  topic: queryText("Topic, category, product, or audience to research on Reddit."),
@@ -200,6 +281,7 @@ var aivRedditTrends = createTool({
200
281
  });
201
282
  var aivYoutubeTrends = createTool({
202
283
  id: "cite42_youtube_trends",
284
+ mcp: readOnly("Get YouTube trend data"),
203
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}`,
204
286
  inputSchema: z.object({
205
287
  topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
@@ -211,7 +293,10 @@ var aivYoutubeTrends = createTool({
211
293
  var channelEnum = z.enum(["ai_search", "seo_keywords", "google_trends", "reddit", "youtube"]);
212
294
  var aivFindContentOpportunities = createTool({
213
295
  id: "cite42_find_content_opportunities",
214
- 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
+ ),
215
300
  inputSchema: z.object({
216
301
  topic: queryText("Market, niche, product category, or audience to research."),
217
302
  audience: optionalShortText("Optional target audience or ICP."),
@@ -224,7 +309,10 @@ var aivFindContentOpportunities = createTool({
224
309
  });
225
310
  var aivAnalyzeTopicDemand = createTool({
226
311
  id: "cite42_analyze_topic_demand",
227
- 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
+ ),
228
316
  inputSchema: z.object({
229
317
  topic: queryText("Topic, keyword, category, or audience demand to analyze."),
230
318
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
@@ -234,7 +322,10 @@ var aivAnalyzeTopicDemand = createTool({
234
322
  });
235
323
  var aivMapAiPrompts = createTool({
236
324
  id: "cite42_map_ai_prompts",
237
- 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
+ ),
238
329
  inputSchema: z.object({
239
330
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
240
331
  audience: optionalShortText("Optional target audience or ICP."),
@@ -247,7 +338,10 @@ var aivMapAiPrompts = createTool({
247
338
  });
248
339
  var aivAnalyzeCompetitorContent = createTool({
249
340
  id: "cite42_analyze_competitor_content",
250
- 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
+ ),
251
345
  inputSchema: z.object({
252
346
  competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
253
347
  topic: z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
@@ -258,7 +352,10 @@ var aivAnalyzeCompetitorContent = createTool({
258
352
  });
259
353
  var aivFindAiCitationGaps = createTool({
260
354
  id: "cite42_find_ai_citation_gaps",
261
- 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
+ ),
262
359
  inputSchema: z.object({
263
360
  query: queryText("AI-search query to inspect for citation gaps."),
264
361
  brand: shortText("Brand or domain that should earn citations."),
@@ -270,7 +367,10 @@ var aivFindAiCitationGaps = createTool({
270
367
  });
271
368
  var aivGenerateContentBrief = createTool({
272
369
  id: "cite42_generate_content_brief",
273
- 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
+ ),
274
374
  inputSchema: z.object({
275
375
  topic: queryText("Topic, keyword, or content idea to brief."),
276
376
  audience: optionalShortText("Optional target audience or ICP."),
@@ -281,6 +381,37 @@ var aivGenerateContentBrief = createTool({
281
381
  execute: async (input) => aivPost("/workflows/content-brief", input)
282
382
  });
283
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
+
284
415
  // src/stdio.ts
285
416
  if (!resolveApiKey()) {
286
417
  console.error(
@@ -288,30 +419,7 @@ if (!resolveApiKey()) {
288
419
  );
289
420
  process.exit(1);
290
421
  }
291
- var server = new MCPServer({
292
- id: "cite42",
293
- name: "Cite42",
294
- version: "0.1.15",
295
- description: "AI market data API: AI search, citations, SEO keywords, trends, and workflows.",
296
- tools: {
297
- cite42_search: aivSearch,
298
- cite42_rankings: aivRankings,
299
- cite42_compare: aivCompare,
300
- cite42_citations: aivCitations,
301
- cite42_sentiment: aivSentiment,
302
- cite42_keywords: aivKeywords,
303
- cite42_trends: aivTrends,
304
- cite42_reddit_trends: aivRedditTrends,
305
- cite42_youtube_trends: aivYoutubeTrends,
306
- cite42_find_content_opportunities: aivFindContentOpportunities,
307
- cite42_analyze_topic_demand: aivAnalyzeTopicDemand,
308
- cite42_map_ai_prompts: aivMapAiPrompts,
309
- cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
310
- cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
311
- cite42_generate_content_brief: aivGenerateContentBrief
312
- }
313
- });
314
- server.startStdio().catch((error) => {
422
+ buildCite42Server("0.1.16").startStdio().catch((error) => {
315
423
  console.error("[@cite42/mcp] Fatal error:", error);
316
424
  process.exit(1);
317
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.15",
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"