@cite42/mcp 0.1.15 → 0.1.17

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