@cite42/mcp 0.5.1 → 0.6.1

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
@@ -4,13 +4,13 @@
4
4
 
5
5
  MCP server for [Cite42, the AI search visibility, SEO keyword & trends API](https://www.cite42.dev): market research tools for your AI agent.
6
6
 
7
- Once connected, Claude Code, Claude Desktop, Codex CLI, or Cursor can:
7
+ Once connected, Claude Code, Claude Desktop, Codex CLI, Cursor, or ChatGPT can:
8
8
 
9
9
  - **Query AI models** (ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews) and see each model's answer and the sources it cites
10
10
  - **Track brand visibility** in AI answers: rankings, head-to-head comparisons, and sentiment
11
11
  - **Schedule weekly or monthly trackers**: one measurement — rankings, comparisons, citations, or sentiment — over a list of prompts, with setup confirmation and a useful email after every run, then ask your agent for stored history
12
12
  - **Pull SEO data**: keyword search volume, CPC, competition, and Google Trends
13
- - **Mine Reddit and YouTube** for audience questions, pain points, and rising content
13
+ - **Research Reddit discussions and YouTube content** for audience questions, pain points, and content ideas
14
14
  - **Run full research workflows**: content gaps, topic demand, AI prompt maps, competitor analysis, and content briefs in a single call
15
15
 
16
16
  ---
@@ -18,53 +18,63 @@ Once connected, Claude Code, Claude Desktop, Codex CLI, or Cursor can:
18
18
  ## Requirements
19
19
 
20
20
  - **A Cite42 account**, free to create at [cite42.dev](https://www.cite42.dev)
21
- - **Node.js 18 or newer and a Cite42 API key** for Claude Code, Codex CLI, and Cursor
21
+ - For the API-key setup only: **Node.js 22.13 or newer and a Cite42 API key**
22
22
 
23
23
  ## Installation
24
24
 
25
- 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`.
25
+ Most clients connect to the hosted server at `https://mcp.cite42.dev` and sign in with your Cite42 account in the browser. There is no API key to create or paste, and nothing to install locally. For CI, servers, or machines without a browser, run this package with an API key instead; see [API key setup](#api-key-setup-ci-servers-no-browser).
26
26
 
27
- ### Step 1: Get your API key for local clients
27
+ ### Claude Code
28
28
 
29
- Skip this step for Claude Desktop; its hosted connector authorizes through sign-in.
30
-
31
- 1. Sign in at [cite42.dev](https://www.cite42.dev)
32
- 2. Open **Dashboard → API Keys**: [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys)
33
- 3. Create a key and copy it (it looks like `cite42_live_...`)
34
-
35
- ### Step 2: Add the server to your client
29
+ ```bash
30
+ claude mcp add --transport http --scope user cite42 https://mcp.cite42.dev && claude mcp login cite42
31
+ ```
36
32
 
37
- Pick **one** of the clients below. For Claude Code, Codex CLI, and Cursor, replace `cite42_live_your_key_here` with your real key.
33
+ Your browser opens to sign in. Then start Claude Code, or restart it if it was already open, and check with `/mcp`.
38
34
 
39
- #### Claude Code
35
+ ### Codex CLI
40
36
 
41
37
  ```bash
42
- claude mcp add cite42 -e CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
38
+ codex mcp add cite42 --url https://mcp.cite42.dev
43
39
  ```
44
40
 
45
- #### Claude Desktop
41
+ Codex opens your browser to sign in. If it doesn't, run `codex mcp login cite42`. Then start a new session.
42
+
43
+ ### Cursor
46
44
 
47
- 1. Open **Settings → Connectors** and click **Add**.
48
- 2. Select **Add custom connector**.
49
- 3. Enter **Cite42** for the name and paste this remote MCP URL:
45
+ [Add to Cursor](https://cursor.com/install-mcp?name=cite42&config=eyJ1cmwiOiJodHRwczovL21jcC5jaXRlNDIuZGV2In0%3D), or add this under **Settings → MCP**, or in `~/.cursor/mcp.json`:
50
46
 
51
- ```text
52
- https://mcp.cite42.dev
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "cite42": { "url": "https://mcp.cite42.dev" }
51
+ }
52
+ }
53
53
  ```
54
54
 
55
- 4. Click **Connect** and complete the Cite42 sign-in. No API key is required.
55
+ Sign in with your Cite42 account when Cursor asks.
56
+
57
+ ### Claude Desktop and Claude.ai
56
58
 
57
- The same hosted-connector steps apply in Claude.ai.
59
+ [Add to Claude](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Cite42&connectorUrl=https%3A%2F%2Fmcp.cite42.dev) opens the connector dialog with Cite42 filled in. Or open **Settings → Connectors → Add custom connector**, name it **Cite42**, and paste `https://mcp.cite42.dev`. Click **Connect** and sign in. The connector appears in both Claude Desktop and Claude.ai.
58
60
 
59
- #### Codex CLI
61
+ ### ChatGPT and Codex Desktop
62
+
63
+ Turn on **Developer mode** in **Settings → Security and login**, then add a plugin under **Settings → Plugins** with the MCP server URL `https://mcp.cite42.dev` and **OAuth**.
64
+
65
+ ### API key setup (CI, servers, no browser)
66
+
67
+ Create a key at [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys) (it looks like `cite42_live_...` and is shown once), then replace `cite42_live_your_key_here` below. The client starts this package on demand; you don't need to keep it running.
60
68
 
61
69
  ```bash
70
+ # Claude Code
71
+ claude mcp add --scope user cite42 -e CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
72
+
73
+ # Codex CLI
62
74
  codex mcp add cite42 --env CITE42_API_KEY=cite42_live_your_key_here -- npx -y @cite42/mcp
63
75
  ```
64
76
 
65
- #### Cursor
66
-
67
- Add this under **Settings → MCP**, or save it to `~/.cursor/mcp.json`:
77
+ For Cursor, save this to `~/.cursor/mcp.json`:
68
78
 
69
79
  ```json
70
80
  {
@@ -80,21 +90,14 @@ Add this under **Settings → MCP**, or save it to `~/.cursor/mcp.json`:
80
90
  }
81
91
  ```
82
92
 
83
- ### Step 3: Restart so the server loads
93
+ Restart the client afterwards: a new terminal or `claude`/`codex` session, or fully quit and reopen Cursor.
84
94
 
85
- 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.
95
+ ### Verify it's running
86
96
 
87
- - **Claude Code / Codex CLI**: exit the current session and open a **new terminal** (or start a new `claude` / `codex` session)
88
- - **Cursor**: fully quit and reopen the app
89
-
90
- 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.
91
-
92
- ### Step 4: Verify it's running
93
-
94
- - **Claude Code:** run `claude mcp list` and check that `cite42` shows as ✔ connected. Or type `/mcp` inside a session.
95
- - **Claude Desktop:** open **Settings → Connectors** and confirm Cite42 is connected.
97
+ - **Claude Code:** run `claude mcp list` and check that `cite42` shows as connected, or type `/mcp` inside a session.
96
98
  - **Codex CLI:** run `codex mcp list`.
97
- - **Cursor:** look for `cite42` in the MCP/tools indicator. 26 tools should be listed.
99
+ - **Cursor:** look for `cite42` in the MCP/tools indicator.
100
+ - **Claude Desktop / Claude.ai:** open **Settings → Connectors** and confirm Cite42 is connected.
98
101
 
99
102
  If it doesn't appear, see [Troubleshooting](#troubleshooting).
100
103
 
@@ -107,7 +110,7 @@ After connecting or restarting, just ask in plain language. Your agent picks the
107
110
  - *"Get search volume, CPC, and related keywords for 'ai seo tools'."*
108
111
  - *"What are people on Reddit complaining about regarding email marketing tools?"*
109
112
  - *"Run a content brief for the topic 'ai website builder'."*
110
- - *"Map the prompts worth tracking for our category, then create a weekly Cite42 rankings tracker for the best five. Show me the estimated credits per run before activating it."*
113
+ - *"Map the prompts worth tracking for our category, then create a weekly Cite42 rankings tracker for the best five. Show me the estimated cost in USD per run before activating it."*
111
114
 
112
115
  ### Example: what a session looks like
113
116
 
@@ -130,11 +133,12 @@ That is the three default surfaces; ask for `fullSweep` to add Claude and Google
130
133
 
131
134
  ## Tools
132
135
 
133
- Cite42 exposes 26 MCP tools: 3 free account tools, 9 data tools, 6 research workflows, and 8 tracker tools.
136
+ Cite42 exposes 29 MCP tools: 4 free account tools (including the compatibility alias), 11 data/report tools, 6 research workflows, and 8 tracker tools.
134
137
 
135
138
  ### Account & pricing (free)
136
139
 
137
- - `cite42_credits`: check the connected account's remaining Cite42 credit balance.
140
+ - `cite42_balance`: check the connected account’s remaining Cite42 balance in USD.
141
+ - `cite42_credits`: compatibility alias for older integrations.
138
142
  - `cite42_pricing`: get current per-tool prices and estimate a planned set of calls.
139
143
  - `cite42_usage`: read recent call status and cost together with the remaining balance.
140
144
 
@@ -145,12 +149,14 @@ Cite42 exposes 26 MCP tools: 3 free account tools, 9 data tools, 6 research work
145
149
  - `cite42_rankings`: measure how brands rank in AI answers, including mention rate, average position, and per-model breakdown.
146
150
  - `cite42_compare`: compare one brand against competitors in AI answers.
147
151
  - `cite42_sentiment`: score positive, neutral, and negative sentiment for a brand, with supporting phrases.
152
+ - `cite42_recommendations`: request an optional paid next-step report using saved results or supplied evidence, with business context. Insufficient evidence and rule-only fallback are free.
148
153
 
149
154
  ### SEO & trend data
150
155
 
151
156
  - `cite42_keywords`: get seed search volume, CPC, and competition, with optional related keyword ideas.
157
+ - `cite42_keywords_batch`: check volume, CPC, and competition for two to 50 seed keywords in one request; billed by outcome (call `cite42_pricing` for rates).
152
158
  - `cite42_trends`: get Google Trends interest over time, related and rising queries, and a trend label.
153
- - `cite42_reddit_trends`: find Reddit audience questions, pain points, rising threads, and product mentions.
159
+ - `cite42_reddit_trends`: find relevant Reddit discussions and supported questions, pain points, and product mentions, with source quotes. Uses a limited sample of titles and excerpts, not complete comment conversations or measured growth. Read `relevantThreads`; `risingThreads` remains a deprecated compatibility alias. Check `analysis.status` and `warnings` for coverage and analysis failures.
154
160
  - `cite42_youtube_trends`: find YouTube rising videos, creator angles, title patterns, and opportunities.
155
161
 
156
162
  ### Research workflows (multi-source, one call)
@@ -175,7 +181,7 @@ Cite42 exposes 26 MCP tools: 3 free account tools, 9 data tools, 6 research work
175
181
 
176
182
  ## Configuration
177
183
 
178
- - `CITE42_API_KEY`: required. Your API key from [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys).
184
+ - `CITE42_API_KEY`: required for the local package. Your API key from [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys).
179
185
  - `CITE42_API_BASE`: optional. Defaults to `https://www.cite42.dev/api/v1`.
180
186
 
181
187
  You can also pass these as CLI flags instead of environment variables:
@@ -186,14 +192,19 @@ npx -y @cite42/mcp --api-key=cite42_live_your_key_here --api-base=https://www.ci
186
192
 
187
193
  ## Billing
188
194
 
189
- Tool and workflow calls consume Cite42 credits from your account. Tracker management and stored-history reads are free; every scheduled or manual tracker run bills one call per saved prompt at the normal rate. 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).
195
+ Tool and workflow calls charge your Cite42 balance in USD. Tracker management and stored-history reads are free; every scheduled or manual tracker run bills one call per saved prompt at the normal rate. Activating or resuming a tracker requires explicit confirmation of the current maximum cost in USD per run and authorizes recurring billed calls until you pause it. Calls fail with a clear error if the API key is missing or your balance runs out. See [www.cite42.dev/pricing](https://www.cite42.dev/pricing).
190
196
 
191
197
  ## Troubleshooting
192
198
 
199
+ For contributor testing, run `npm run inspect` to open the official MCP Inspector with local
200
+ stdio, published npm, and local OAuth targets. Run `npm run test:stdio` for a free built-executable
201
+ smoke test against a loopback fake API. See [local MCP testing](../../docs/mcp-testing.md) in the repository.
202
+
193
203
  - **Server not listed after setup**: make sure you fully restarted the client (new terminal for CLI clients, full quit for desktop apps).
194
- - **`command not found: npx`**: install Node.js 18+ from [nodejs.org](https://nodejs.org), then restart your terminal.
204
+ - **"No Cite42 account is linked to this sign-in"**: you signed in with a different account than the one you use at cite42.dev. In Claude Code run `claude mcp logout cite42`, then `claude mcp login cite42` and pick the same sign-in method as on the dashboard.
205
+ - **`command not found: npx`**: install Node.js 22.13+ from [nodejs.org](https://nodejs.org), then restart your terminal.
195
206
  - **Tool calls fail with an auth error**: check the key was passed correctly (`CITE42_API_KEY`, no quotes or trailing spaces) and is active at [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys).
196
- - **Tool calls fail with a credits error**: top up credits at [www.cite42.dev/app](https://www.cite42.dev/app).
207
+ - **Tool calls fail with an insufficient-balance error**: add funds at [www.cite42.dev/app](https://www.cite42.dev/app).
197
208
 
198
209
  ## Links
199
210
 
@@ -201,3 +212,53 @@ Tool and workflow calls consume Cite42 credits from your account. Tracker manage
201
212
  - API keys: [Create a Cite42 API key](https://www.cite42.dev/app/keys)
202
213
  - Docs: [Cite42 API & MCP documentation](https://www.cite42.dev/docs)
203
214
  - Pricing: [Cite42 pay-per-call pricing](https://www.cite42.dev/pricing)
215
+
216
+ ### USD billing and older clients
217
+
218
+ Quote the `cite42Billing` block for the amount charged and remaining Cite42 balance.
219
+ Its `chargedUsd` and `remainingUsd` values are decimal strings in USD. Legacy
220
+ `cite42Credits` numbers keep their original units for compatibility.
221
+
222
+ Call `cite42_pricing` with `{ "currency": "USD" }` for dollar prices. With no argument,
223
+ the legacy table is preserved and dollar prices are in the `usd` block.
224
+ REST clients can use `GET /v1/pricing?currency=USD`; the default REST response remains
225
+ compatible with existing installations. Update the MCP package to receive the new
226
+ USD guidance and `cite42_balance` tool. Tracker activation still accepts the exact
227
+ integer `maxCostMicroPerRun` returned by the API; present its USD equivalent to users.
228
+
229
+ ## Optional AI visibility recommendations
230
+
231
+ `cite42_rankings`, `cite42_compare`, `cite42_citations` and `cite42_sentiment` include a `metrics`
232
+ block. Show mention/citation percentages with their counts (for example, **66.7% — 2 of 3 checked
233
+ answers**). Sentiment percentages describe AI-estimated tone, not the percentage of positive answers.
234
+ Failed, empty and incomplete observations are excluded; no usable sample is unavailable, not zero.
235
+ Only comparable tracker runs provide automatic history. Each reported run includes `metrics` grouped
236
+ by primitive and brand/watched URL, with `current`, `previous`, `previousRunId`, and `changes`.
237
+ Each group states `models` and `methodologyVersion`. Its `comparison` is `comparable`, `baseline`
238
+ (no earlier run, or a new measurement baseline after an old-format run), or `unavailable`;
239
+ `comparisonReason` explains skipped comparisons. A successful run with unavailable comparison data
240
+ is not a failed run, and its email does not claim "no changes". Completed manual-run replays include
241
+ the same immediate predecessor as tracker reports.
242
+ Citation groups preserve the original watched URL; normalized identities are only used for matching.
243
+ Counts sum distinct prompts: four prompts × five surfaces can supply 20 answers. Comparisons require
244
+ the complete prompt/configuration set, methodology and complete coverage to match the preceding run.
245
+ Individual prompts remain small samples (at most five surfaces); percentage-point changes are withheld
246
+ below 10 answers per group in each run. This guardrail does not establish statistical significance.
247
+
248
+ `cite42_recommendations` is a separate, optional paid report. Use exactly one `requestId` returned by
249
+ an earlier call, a user-owned `trackerRunId`, or a structured `evidence` array. Optional `context`
250
+ accepts brand, domain, audience and goal for this request only. Check `cite42_pricing` for the current
251
+ flat report price and obtain the user's agreement before running the add-on. Insufficient evidence
252
+ and rule-only fallback are free.
253
+
254
+ For rankings evidence, provide `context.brand` to identify the tracked brand you want advice for.
255
+ Without it, the measurements remain available but the tool asks which brand to focus on for free;
256
+ it never guesses ownership or advises you to improve a competitor's offering. Comparison evidence
257
+ already identifies its target through `input.brand`.
258
+
259
+ Sources must be complete and no more than 30 days old, within 20 observations and 64KB. Supported
260
+ successful workflow steps can be reused. The tool does not inspect websites, retrieve host memory,
261
+ perform fresh research, recommend keywords, or activate paid tracking. Suggestions are labelled
262
+ inferred and do not establish website defects or causes. Tracker follow-up requires a cadence and
263
+ timezone choice plus separate activation approval. Identical completed retries are free for two
264
+ minutes; an in-flight duplicate is refused without another charge and can be retried shortly.
package/dist/stdio.cjs CHANGED
@@ -4,6 +4,19 @@
4
4
  // src/client.ts
5
5
  var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
6
6
  var MICRO_CREDITS_PER_CREDIT = 1e3;
7
+ function toUsd(micro) {
8
+ const [whole, fraction = ""] = (micro / 1e5).toFixed(5).split(".");
9
+ return `${whole}.${fraction.slice(0, Math.max(2, fraction.replace(/0+$/, "").length))}`;
10
+ }
11
+ function toCite42Balance(micro) {
12
+ return {
13
+ cite42Billing: {
14
+ currency: "USD",
15
+ remainingUsd: toUsd(micro),
16
+ message: `Cite42 balance: $${toUsd(micro)} USD remaining. This balance check was free.`
17
+ }
18
+ };
19
+ }
7
20
  function isJsonObject(value) {
8
21
  return typeof value === "object" && value !== null && !Array.isArray(value);
9
22
  }
@@ -30,19 +43,33 @@ function toCite42McpResult(value) {
30
43
  if (typeof cost !== "number" || !Number.isFinite(cost) || typeof balance !== "number" || !Number.isFinite(balance)) {
31
44
  return result;
32
45
  }
46
+ const originalCost = isJsonObject(billing.originalCost) ? billing.originalCost.microCredits : void 0;
47
+ const replay = typeof result.replayOf === "string" && typeof originalCost === "number" && Number.isFinite(originalCost);
48
+ const lowBalanceNotice = typeof warning === "string" ? " Low Cite42 balance." : "";
49
+ const cite42Billing = {
50
+ currency: "USD",
51
+ chargedUsd: toUsd(cost),
52
+ remainingUsd: toUsd(balance),
53
+ ...replay ? { originalChargedUsd: toUsd(originalCost) } : {},
54
+ message: (replay ? `Replay of an identical request from the last 2 minutes. This attempt was free; $${toUsd(originalCost)} USD was charged once, by the original Cite42 call.` : `Cite42: $${toUsd(cost)} USD charged; $${toUsd(balance)} USD remaining.`) + lowBalanceNotice
55
+ };
33
56
  const lowCredits = typeof warning === "string";
34
57
  const message = `Cite42 credits: ${formatCredits(cost)} charged; ${formatCredits(balance)} remaining.`;
35
58
  return {
36
59
  ...result,
60
+ cite42Billing,
61
+ // Legacy numeric fields retain their original units for existing integrations.
37
62
  cite42Credits: {
38
63
  charged: toCredits(cost),
39
64
  remaining: toCredits(balance),
40
- message: lowCredits ? `${message} Low Cite42 credit balance.` : message
65
+ ...replay ? { originalCharged: toCredits(originalCost) } : {},
66
+ message: replay ? `Replay of an identical request from the last 2 minutes. This attempt was free; ${toCredits(originalCost)} credits were charged once, by the original call.` : lowCredits ? `${message} Low Cite42 credit balance.` : message
41
67
  }
42
68
  };
43
69
  }
44
70
  function toCite42CreditsBalance(balanceMicroCredits) {
45
71
  return {
72
+ ...toCite42Balance(balanceMicroCredits),
46
73
  cite42Credits: {
47
74
  remaining: toCredits(balanceMicroCredits),
48
75
  message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
@@ -54,10 +81,10 @@ function errorMessageForMcp(status, path, body) {
54
81
  const payload = JSON.parse(body);
55
82
  if (isJsonObject(payload) && isJsonObject(payload.error) && typeof payload.error.code === "string") {
56
83
  if (payload.error.code === "daily_limit_exceeded" || payload.error.code === "monthly_limit_exceeded") {
57
- return "Cite42 credit limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
84
+ return "Cite42 spending limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
58
85
  }
59
86
  if (payload.error.code === "insufficient_credits") {
60
- return "Insufficient Cite42 credits. Top up at https://www.cite42.dev/app/billing.";
87
+ return "Insufficient Cite42 balance. Add funds at https://www.cite42.dev/app/billing.";
61
88
  }
62
89
  }
63
90
  } catch {
@@ -90,8 +117,8 @@ function getApiKey() {
90
117
  function getBaseUrl() {
91
118
  return readFlag("api-base") ?? process.env.CITE42_API_BASE ?? DEFAULT_BASE_URL;
92
119
  }
93
- async function aivPost(path, body) {
94
- return aivWrite("POST", path, body);
120
+ async function aivPost(path, body, signal) {
121
+ return aivWrite("POST", path, body, signal);
95
122
  }
96
123
  async function aivPatch(path, body) {
97
124
  return aivWrite("PATCH", path, body);
@@ -108,7 +135,8 @@ async function aivDelete(path) {
108
135
  }
109
136
  return toCite42McpResult(await res.json());
110
137
  }
111
- async function aivWrite(method, path, body) {
138
+ async function aivWrite(method, path, body, signal) {
139
+ signal?.throwIfAborted();
112
140
  const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
113
141
  const res = await fetch(url, {
114
142
  method,
@@ -116,7 +144,8 @@ async function aivWrite(method, path, body) {
116
144
  "Content-Type": "application/json",
117
145
  Authorization: `Bearer ${getApiKey()}`
118
146
  },
119
- body: JSON.stringify(body)
147
+ body: JSON.stringify(body),
148
+ signal
120
149
  });
121
150
  if (!res.ok) {
122
151
  const text = await res.text();
@@ -149,8 +178,8 @@ var SHORT_TEXT_MAX_CHARS = 200;
149
178
  var TIMEFRAME_MAX_CHARS = 80;
150
179
  var MODEL_SELECTION_DESCRIPTION = 'Optional model subset. Omit to query the default surfaces (chatgpt, perplexity, gemini). Pass ["perplexity"] or ["perplexity","claude"] when the user names specific surfaces or wants to reduce cost. The full set is chatgpt, claude, perplexity, gemini, google_ai_overview \u2014 to query all of them, set fullSweep instead of listing them.';
151
180
  var FULL_SWEEP_DESCRIPTION = "Query all five AI surfaces instead of the three default ones, adding claude and google_ai_overview. Costs more per call. Use it when the user wants complete cross-model coverage, is comparing surfaces against each other, or asks specifically about Claude or Google AI Overviews. Ignored when models is given.";
152
- 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.";
153
- var MODEL_PRICING = "Pricing is per selected AI surface. Omitting models queries the three default surfaces (chatgpt, perplexity, gemini); fullSweep: true queries all five, adding claude and google_ai_overview for more credits. Use the models field when the user names a surface or asks to reduce credit use, and call cite42_pricing for exact current rates.";
181
+ 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 $5 USD, estimate the cost in USD and ask the user before continuing. Quote Cite42 costs and balances in USD using cite42Billing or the USD pricing table. Legacy credit fields are for compatibility only. These funds belong to Cite42, not the host or model provider. AI visibility metrics include counts; quote them with percentages, not as a market-wide score. Optional recommendations are a separate paid call, never automatic. After a visibility result, offer recommendations using its requestId; run only when the user requests advice and knows the additional cost. Free: cite42_balance for the remaining balance, cite42_pricing for exact per-tool prices and cost estimates, cite42_usage for recent calls.";
182
+ var MODEL_PRICING = "Pricing is per selected AI surface. Omitting models queries the three default surfaces (chatgpt, perplexity, gemini); fullSweep: true queries all five, adding claude and google_ai_overview at a higher cost. Use the models field when the user names a surface or asks to reduce spending, and call cite42_pricing for exact current rates.";
154
183
  var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
155
184
  var KEYWORDS_PRICING = "Pricing: billed by outcome \u2014 cheapest when the seed has no search volume, more when it has metrics, most with ideas:true. Call cite42_pricing for exact current rates.";
156
185
  var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the tools that run successfully and the selected model rates.";
@@ -177,16 +206,59 @@ function queryText(description) {
177
206
  function shortText(description) {
178
207
  return import_zod.z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).describe(description);
179
208
  }
209
+ var aivRecommendations = (0, import_tools.createTool)({
210
+ id: "cite42_recommendations",
211
+ mcp: readOnly("Recommend AI visibility next steps"),
212
+ description: "Optional evidence-backed AI visibility recommendations. Flat price from cite42_pricing per completed AI report; insufficient evidence and rule-only fallback are free. Supports rankings, compare, citations and sentiment, including usable workflow steps. No crawling, fresh research, keyword advice or tracker activation. " + MCP_USAGE_GUIDANCE,
213
+ inputSchema: import_zod.z.object({
214
+ requestId: import_zod.z.string().trim().min(1).max(128).optional().describe("A requestId from your own successful Cite42 call within the last 30 days."),
215
+ trackerRunId: import_zod.z.string().trim().min(1).max(128).optional().describe("A specific finished run of your own tracker within the last 30 days."),
216
+ evidence: import_zod.z.array(import_zod.z.object({
217
+ primitive: import_zod.z.enum(["rankings", "compare", "citations", "sentiment"]),
218
+ input: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()),
219
+ result: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()),
220
+ measuredAt: import_zod.z.string().datetime()
221
+ })).min(1).max(20).optional(),
222
+ context: import_zod.z.object({
223
+ brand: import_zod.z.string().trim().min(1).max(200).optional(),
224
+ domain: import_zod.z.string().trim().min(1).max(250).optional(),
225
+ audience: import_zod.z.string().trim().min(1).max(400).optional(),
226
+ goal: import_zod.z.string().trim().min(1).max(400).optional()
227
+ }).optional()
228
+ }).superRefine((input, ctx) => {
229
+ if ([input.requestId, input.trackerRunId, input.evidence].filter((value) => value !== void 0).length !== 1) {
230
+ ctx.addIssue({ code: "custom", message: "Provide exactly one of requestId, trackerRunId or evidence." });
231
+ }
232
+ if (new TextEncoder().encode(JSON.stringify(input)).length > 64 * 1024) {
233
+ ctx.addIssue({ code: "custom", message: "Recommendation input must fit within 64KB." });
234
+ }
235
+ }),
236
+ execute: async (input) => aivPost("/recommendations", input)
237
+ });
180
238
  function optionalShortText(description) {
181
239
  return import_zod.z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).optional().describe(description);
182
240
  }
183
241
  var modelsInput = import_zod.z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
184
242
  var fullSweepInput = import_zod.z.boolean().optional().describe(FULL_SWEEP_DESCRIPTION);
185
243
  var competitorList = import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
244
+ var aivBalance = (0, import_tools.createTool)({
245
+ id: "cite42_balance",
246
+ mcp: readOnly("Check Cite42 balance", false),
247
+ description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
248
+ inputSchema: import_zod.z.object({}),
249
+ execute: async () => {
250
+ const usage = await aivGet("/usage");
251
+ const microCredits = usage.balance?.microCredits;
252
+ if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
253
+ throw new Error("Cite42 usage response did not include a balance.");
254
+ }
255
+ return toCite42Balance(microCredits);
256
+ }
257
+ });
186
258
  var aivCredits = (0, import_tools.createTool)({
187
259
  id: "cite42_credits",
188
- mcp: readOnly("Check Cite42 credit balance", false),
189
- description: "Check the remaining Cite42 credit balance for the connected account. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
260
+ mcp: readOnly("Check Cite42 balance (legacy alias)", false),
261
+ description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
190
262
  inputSchema: import_zod.z.object({}),
191
263
  execute: async () => {
192
264
  const usage = await aivGet("/usage");
@@ -200,14 +272,16 @@ var aivCredits = (0, import_tools.createTool)({
200
272
  var aivPricing = (0, import_tools.createTool)({
201
273
  id: "cite42_pricing",
202
274
  mcp: readOnly("Get Cite42 price table", false),
203
- 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,
204
- inputSchema: import_zod.z.object({}),
205
- execute: async () => aivGet("/pricing")
275
+ description: 'Get the exact Cite42 price table. Pass currency: USD for dollar prices; otherwise read the usd block (legacy fields are compatibility only): 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. ' + MCP_USAGE_GUIDANCE,
276
+ inputSchema: import_zod.z.object({
277
+ currency: import_zod.z.enum(["USD", "credits"]).optional().describe("Use USD for dollar prices. Omit only for legacy compatibility.")
278
+ }),
279
+ execute: async (input) => input.currency === "USD" ? aivGet("/pricing?currency=USD") : { ...await aivGet("/pricing"), usd: await aivGet("/pricing?currency=USD") }
206
280
  });
207
281
  var aivUsage = (0, import_tools.createTool)({
208
282
  id: "cite42_usage",
209
283
  mcp: readOnly("Get recent Cite42 usage", false),
210
- 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,
284
+ description: "Get recent Cite42 usage for the connected account: the last calls with their cost and status, plus the remaining balance in USD. Free. " + MCP_USAGE_GUIDANCE,
211
285
  inputSchema: import_zod.z.object({}),
212
286
  execute: async () => {
213
287
  const usage = await aivGet("/usage");
@@ -221,6 +295,7 @@ var aivUsage = (0, import_tools.createTool)({
221
295
  endpoint: row.endpoint,
222
296
  status: row.status,
223
297
  credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
298
+ costUsd: typeof row.costMicro === "number" ? toUsd(row.costMicro) : void 0,
224
299
  at: row.createdAt
225
300
  }))
226
301
  };
@@ -302,6 +377,23 @@ var aivKeywords = (0, import_tools.createTool)({
302
377
  }),
303
378
  execute: async (input) => aivPost("/keywords", input)
304
379
  });
380
+ var aivKeywordsBatch = (0, import_tools.createTool)({
381
+ id: "cite42_keywords_batch",
382
+ mcp: readOnly("Get batch SEO keyword data"),
383
+ description: `Check search volume, CPC, and competition for two to 50 distinct seed keywords in one request. No related ideas. Billed by outcome; call cite42_pricing for current rates. ${MCP_USAGE_GUIDANCE}`,
384
+ inputSchema: import_zod.z.object({
385
+ seeds: import_zod.z.array(import_zod.z.string().trim().min(1).max(80).refine(
386
+ (seed) => seed.split(/\s+/).length <= 10,
387
+ "Each keyword must contain at most 10 words."
388
+ )).min(2).max(50).refine(
389
+ (seeds) => new Set(seeds.map((seed) => seed.toLowerCase().replace(/\s+/g, " "))).size === seeds.length,
390
+ "Keywords must be distinct."
391
+ ).describe("Two to 50 distinct seed keywords."),
392
+ country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
393
+ language: import_zod.z.string().trim().max(8).optional().describe("Optional ISO-2 language code.")
394
+ }),
395
+ execute: async (input) => aivPost("/keywords/batch", input)
396
+ });
305
397
  var aivTrends = (0, import_tools.createTool)({
306
398
  id: "cite42_trends",
307
399
  mcp: readOnly("Get Google Trends data"),
@@ -315,14 +407,14 @@ var aivTrends = (0, import_tools.createTool)({
315
407
  });
316
408
  var aivRedditTrends = (0, import_tools.createTool)({
317
409
  id: "cite42_reddit_trends",
318
- mcp: readOnly("Get Reddit trend data"),
319
- 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}`,
410
+ mcp: readOnly("Find Reddit discussions"),
411
+ description: `Discover relevant Reddit discussions through Gemini Google Search. Returns labeled model summaries, citations and coverage warnings, with no author quotes or quote-based insights. Does not measure growth or read complete comment conversations. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
320
412
  inputSchema: import_zod.z.object({
321
413
  topic: queryText("Topic, category, product, or audience to research on Reddit."),
322
414
  subreddits: import_zod.z.array(shortText("Subreddit name without r/.")).max(20).optional().describe("Optional subreddit names, without r/."),
323
415
  timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Conversation window, e.g. "past_7_days", "past_30_days", "past_90_days".')
324
416
  }),
325
- execute: async (input) => aivPost("/reddit/trends", input)
417
+ execute: async (input, context) => aivPost("/reddit/trends", input, context?.abortSignal ?? context?.mcp?.extra.signal)
326
418
  });
327
419
  var aivYoutubeTrends = (0, import_tools.createTool)({
328
420
  id: "cite42_youtube_trends",
@@ -350,7 +442,7 @@ var aivFindContentOpportunities = (0, import_tools.createTool)({
350
442
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
351
443
  channels: import_zod.z.array(channelEnum).optional().describe("Optional data sources to include.")
352
444
  }),
353
- execute: async (input) => aivPost("/workflows/content-opportunities", input)
445
+ execute: async (input, context) => aivPost("/workflows/content-opportunities", input, context?.abortSignal ?? context?.mcp?.extra.signal)
354
446
  });
355
447
  var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
356
448
  id: "cite42_analyze_topic_demand",
@@ -363,13 +455,13 @@ var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
363
455
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
364
456
  timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
365
457
  }),
366
- execute: async (input) => aivPost("/workflows/topic-demand", input)
458
+ execute: async (input, context) => aivPost("/workflows/topic-demand", input, context?.abortSignal ?? context?.mcp?.extra.signal)
367
459
  });
368
460
  var aivMapAiPrompts = (0, import_tools.createTool)({
369
461
  id: "cite42_map_ai_prompts",
370
462
  mcp: readOnly("Map buyer prompts for AI search"),
371
463
  description: workflowDescription(
372
- "Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data. The prompts it returns are exactly what cite42_tracker_create takes, so offer to start tracking the best of them once the user has seen the map."
464
+ "Run the AI-prompt-map workflow: research for a list of buyer prompts worth tracking, from AI search, SEO demand, trends, citations, Reddit, and YouTube data. It returns evidence and a suggested prompt, not a finished list: write the prompts from it, then offer to track the best of them with cite42_tracker_create once the user has seen them."
373
465
  ),
374
466
  inputSchema: import_zod.z.object({
375
467
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
@@ -379,7 +471,7 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
379
471
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
380
472
  timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
381
473
  }),
382
- execute: async (input) => aivPost("/workflows/ai-prompt-map", input)
474
+ execute: async (input, context) => aivPost("/workflows/ai-prompt-map", input, context?.abortSignal ?? context?.mcp?.extra.signal)
383
475
  });
384
476
  var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
385
477
  id: "cite42_analyze_competitor_content",
@@ -393,7 +485,7 @@ var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
393
485
  brand: optionalShortText("Optional primary brand/domain to compare against."),
394
486
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
395
487
  }),
396
- execute: async (input) => aivPost("/workflows/competitor-content", input)
488
+ execute: async (input, context) => aivPost("/workflows/competitor-content", input, context?.abortSignal ?? context?.mcp?.extra.signal)
397
489
  });
398
490
  var aivFindAiCitationGaps = (0, import_tools.createTool)({
399
491
  id: "cite42_find_ai_citation_gaps",
@@ -409,13 +501,13 @@ var aivFindAiCitationGaps = (0, import_tools.createTool)({
409
501
  models: modelsInput,
410
502
  fullSweep: fullSweepInput
411
503
  }),
412
- execute: async (input) => aivPost("/workflows/ai-citation-gaps", input)
504
+ execute: async (input, context) => aivPost("/workflows/ai-citation-gaps", input, context?.abortSignal ?? context?.mcp?.extra.signal)
413
505
  });
414
506
  var aivGenerateContentBrief = (0, import_tools.createTool)({
415
507
  id: "cite42_generate_content_brief",
416
508
  mcp: readOnly("Generate content brief data"),
417
509
  description: workflowDescription(
418
- "Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data."
510
+ "Run the content-brief workflow: research for a content brief from AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals. It returns evidence and a suggested prompt; write the brief from it."
419
511
  ),
420
512
  inputSchema: import_zod.z.object({
421
513
  topic: queryText("Topic, keyword, or content idea to brief."),
@@ -424,7 +516,7 @@ var aivGenerateContentBrief = (0, import_tools.createTool)({
424
516
  goal: optionalShortText("Optional content goal, e.g. rank, earn citations, convert."),
425
517
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
426
518
  }),
427
- execute: async (input) => aivPost("/workflows/content-brief", input)
519
+ execute: async (input, context) => aivPost("/workflows/content-brief", input, context?.abortSignal ?? context?.mcp?.extra.signal)
428
520
  });
429
521
  var trackerIdInput = import_zod.z.string().trim().min(1).max(128).describe("Cite42 tracker ID returned by a tracker management or report tool.");
430
522
  var trackerCadenceInput = import_zod.z.object({
@@ -478,11 +570,11 @@ var trackerDefinitionFields = {
478
570
  "AI surfaces to query for every prompt. Omit for the three defaults (chatgpt, perplexity, gemini), or list surfaces explicitly \u2014 including all five \u2014 to fix the recurring cost."
479
571
  )
480
572
  };
481
- var TRACKER_BILLING_DESCRIPTION = "Tracker management and stored-report reads are free. Each scheduled or manual run bills one call per prompt at the normal per-model rates; the activation response and tracker record expose the accepted maximum cost per run.";
573
+ var TRACKER_BILLING_DESCRIPTION = "Tracker management and stored-report reads are free. Each scheduled or manual run bills one call per prompt at the normal per-model rates; the activation response and tracker record expose the accepted maximum cost per run. Present the returned USD amounts to users; activation still takes the exact estimatedCostMicroPerRun integer returned by the tracker.";
482
574
  var aivTrackerCreate = (0, import_tools.createTool)({
483
575
  id: "cite42_tracker_create",
484
576
  mcp: mutation("Create Cite42 tracker draft"),
485
- description: "Create a draft weekly or monthly tracker: one measurement \u2014 rankings, compare, citations, or sentiment \u2014 applied to a list of prompts, with the brand, competitors and AI surfaces set once for all of them. When the user has no prompt list yet, run cite42_map_ai_prompts first and track the prompts it recommends. Creation does not start recurring calls. " + TRACKER_BILLING_DESCRIPTION,
577
+ description: "Create a draft weekly or monthly tracker: one measurement \u2014 rankings, compare, citations, or sentiment \u2014 applied to a list of prompts, with the brand, competitors and AI surfaces set once for all of them. When the user has no prompt list yet, run cite42_map_ai_prompts first and track the prompts you write from its research. Creation does not start recurring calls. " + TRACKER_BILLING_DESCRIPTION,
486
578
  inputSchema: import_zod.z.object(trackerDefinitionFields),
487
579
  execute: async (input) => aivPost("/trackers", input)
488
580
  });
@@ -509,7 +601,7 @@ var aivTrackerActivate = (0, import_tools.createTool)({
509
601
  var aivTrackerPause = (0, import_tools.createTool)({
510
602
  id: "cite42_tracker_pause",
511
603
  mcp: mutation("Pause Cite42 tracker", { idempotentHint: true }),
512
- description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free: costs 0 credits.",
604
+ description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free.",
513
605
  inputSchema: import_zod.z.object({ trackerId: trackerIdInput }),
514
606
  execute: async ({ trackerId }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/pause`, {})
515
607
  });
@@ -519,7 +611,7 @@ var aivTrackerDelete = (0, import_tools.createTool)({
519
611
  destructiveHint: true,
520
612
  idempotentHint: true
521
613
  }),
522
- 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.",
614
+ 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 billing ledger records are retained. Free.",
523
615
  inputSchema: import_zod.z.object({ trackerId: trackerIdInput }),
524
616
  execute: async ({ trackerId }) => aivDelete(`/trackers/${encodeURIComponent(trackerId)}`)
525
617
  });
@@ -539,14 +631,14 @@ var aivTrackerRun = (0, import_tools.createTool)({
539
631
  var aivTrackers = (0, import_tools.createTool)({
540
632
  id: "cite42_trackers",
541
633
  mcp: readOnly("List Cite42 trackers", false),
542
- description: "List the connected account\u2019s trackers with status, cadence, tool configuration, cost estimate, accepted per-run maximum, and next/last run times. Free: costs 0 credits.",
634
+ description: "List the connected account\u2019s trackers with status, cadence, tool configuration, cost estimate, accepted per-run maximum, and next/last run times. Free.",
543
635
  inputSchema: import_zod.z.object({}),
544
636
  execute: async () => aivGet("/trackers")
545
637
  });
546
638
  var aivTrackerReport = (0, import_tools.createTool)({
547
639
  id: "cite42_tracker_report",
548
640
  mcp: readOnly("Read Cite42 tracker report", false),
549
- description: "Read bounded stored run history for one tracker, including per-tool results, changes from the prior comparable observation, statuses, sampled times, and costs. Free: costs 0 credits.",
641
+ description: "Read bounded stored run history for one tracker, including per-tool results, changes from the prior comparable observation, statuses, sampled times, and costs. Free.",
550
642
  inputSchema: import_zod.z.object({
551
643
  trackerId: trackerIdInput,
552
644
  limit: import_zod.z.number().int().min(1).max(20).optional().default(8).describe("Most recent runs to return, capped for LLM context.")
@@ -556,6 +648,7 @@ var aivTrackerReport = (0, import_tools.createTool)({
556
648
 
557
649
  // src/server.ts
558
650
  var CITE42_TOOLS = {
651
+ cite42_balance: aivBalance,
559
652
  cite42_credits: aivCredits,
560
653
  cite42_pricing: aivPricing,
561
654
  cite42_usage: aivUsage,
@@ -564,7 +657,9 @@ var CITE42_TOOLS = {
564
657
  cite42_compare: aivCompare,
565
658
  cite42_citations: aivCitations,
566
659
  cite42_sentiment: aivSentiment,
660
+ cite42_recommendations: aivRecommendations,
567
661
  cite42_keywords: aivKeywords,
662
+ cite42_keywords_batch: aivKeywordsBatch,
568
663
  cite42_trends: aivTrends,
569
664
  cite42_reddit_trends: aivRedditTrends,
570
665
  cite42_youtube_trends: aivYoutubeTrends,
@@ -600,7 +695,7 @@ if (!resolveApiKey()) {
600
695
  );
601
696
  process.exit(1);
602
697
  }
603
- buildCite42Server("0.5.1").startStdio().catch((error) => {
698
+ buildCite42Server("0.6.1").startStdio().catch((error) => {
604
699
  console.error("[@cite42/mcp] Fatal error:", error);
605
700
  process.exit(1);
606
701
  });
package/dist/stdio.mjs CHANGED
@@ -3,6 +3,19 @@
3
3
  // src/client.ts
4
4
  var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
5
5
  var MICRO_CREDITS_PER_CREDIT = 1e3;
6
+ function toUsd(micro) {
7
+ const [whole, fraction = ""] = (micro / 1e5).toFixed(5).split(".");
8
+ return `${whole}.${fraction.slice(0, Math.max(2, fraction.replace(/0+$/, "").length))}`;
9
+ }
10
+ function toCite42Balance(micro) {
11
+ return {
12
+ cite42Billing: {
13
+ currency: "USD",
14
+ remainingUsd: toUsd(micro),
15
+ message: `Cite42 balance: $${toUsd(micro)} USD remaining. This balance check was free.`
16
+ }
17
+ };
18
+ }
6
19
  function isJsonObject(value) {
7
20
  return typeof value === "object" && value !== null && !Array.isArray(value);
8
21
  }
@@ -29,19 +42,33 @@ function toCite42McpResult(value) {
29
42
  if (typeof cost !== "number" || !Number.isFinite(cost) || typeof balance !== "number" || !Number.isFinite(balance)) {
30
43
  return result;
31
44
  }
45
+ const originalCost = isJsonObject(billing.originalCost) ? billing.originalCost.microCredits : void 0;
46
+ const replay = typeof result.replayOf === "string" && typeof originalCost === "number" && Number.isFinite(originalCost);
47
+ const lowBalanceNotice = typeof warning === "string" ? " Low Cite42 balance." : "";
48
+ const cite42Billing = {
49
+ currency: "USD",
50
+ chargedUsd: toUsd(cost),
51
+ remainingUsd: toUsd(balance),
52
+ ...replay ? { originalChargedUsd: toUsd(originalCost) } : {},
53
+ message: (replay ? `Replay of an identical request from the last 2 minutes. This attempt was free; $${toUsd(originalCost)} USD was charged once, by the original Cite42 call.` : `Cite42: $${toUsd(cost)} USD charged; $${toUsd(balance)} USD remaining.`) + lowBalanceNotice
54
+ };
32
55
  const lowCredits = typeof warning === "string";
33
56
  const message = `Cite42 credits: ${formatCredits(cost)} charged; ${formatCredits(balance)} remaining.`;
34
57
  return {
35
58
  ...result,
59
+ cite42Billing,
60
+ // Legacy numeric fields retain their original units for existing integrations.
36
61
  cite42Credits: {
37
62
  charged: toCredits(cost),
38
63
  remaining: toCredits(balance),
39
- message: lowCredits ? `${message} Low Cite42 credit balance.` : message
64
+ ...replay ? { originalCharged: toCredits(originalCost) } : {},
65
+ message: replay ? `Replay of an identical request from the last 2 minutes. This attempt was free; ${toCredits(originalCost)} credits were charged once, by the original call.` : lowCredits ? `${message} Low Cite42 credit balance.` : message
40
66
  }
41
67
  };
42
68
  }
43
69
  function toCite42CreditsBalance(balanceMicroCredits) {
44
70
  return {
71
+ ...toCite42Balance(balanceMicroCredits),
45
72
  cite42Credits: {
46
73
  remaining: toCredits(balanceMicroCredits),
47
74
  message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
@@ -53,10 +80,10 @@ function errorMessageForMcp(status, path, body) {
53
80
  const payload = JSON.parse(body);
54
81
  if (isJsonObject(payload) && isJsonObject(payload.error) && typeof payload.error.code === "string") {
55
82
  if (payload.error.code === "daily_limit_exceeded" || payload.error.code === "monthly_limit_exceeded") {
56
- return "Cite42 credit limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
83
+ return "Cite42 spending limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
57
84
  }
58
85
  if (payload.error.code === "insufficient_credits") {
59
- return "Insufficient Cite42 credits. Top up at https://www.cite42.dev/app/billing.";
86
+ return "Insufficient Cite42 balance. Add funds at https://www.cite42.dev/app/billing.";
60
87
  }
61
88
  }
62
89
  } catch {
@@ -89,8 +116,8 @@ function getApiKey() {
89
116
  function getBaseUrl() {
90
117
  return readFlag("api-base") ?? process.env.CITE42_API_BASE ?? DEFAULT_BASE_URL;
91
118
  }
92
- async function aivPost(path, body) {
93
- return aivWrite("POST", path, body);
119
+ async function aivPost(path, body, signal) {
120
+ return aivWrite("POST", path, body, signal);
94
121
  }
95
122
  async function aivPatch(path, body) {
96
123
  return aivWrite("PATCH", path, body);
@@ -107,7 +134,8 @@ async function aivDelete(path) {
107
134
  }
108
135
  return toCite42McpResult(await res.json());
109
136
  }
110
- async function aivWrite(method, path, body) {
137
+ async function aivWrite(method, path, body, signal) {
138
+ signal?.throwIfAborted();
111
139
  const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
112
140
  const res = await fetch(url, {
113
141
  method,
@@ -115,7 +143,8 @@ async function aivWrite(method, path, body) {
115
143
  "Content-Type": "application/json",
116
144
  Authorization: `Bearer ${getApiKey()}`
117
145
  },
118
- body: JSON.stringify(body)
146
+ body: JSON.stringify(body),
147
+ signal
119
148
  });
120
149
  if (!res.ok) {
121
150
  const text = await res.text();
@@ -148,8 +177,8 @@ var SHORT_TEXT_MAX_CHARS = 200;
148
177
  var TIMEFRAME_MAX_CHARS = 80;
149
178
  var MODEL_SELECTION_DESCRIPTION = 'Optional model subset. Omit to query the default surfaces (chatgpt, perplexity, gemini). Pass ["perplexity"] or ["perplexity","claude"] when the user names specific surfaces or wants to reduce cost. The full set is chatgpt, claude, perplexity, gemini, google_ai_overview \u2014 to query all of them, set fullSweep instead of listing them.';
150
179
  var FULL_SWEEP_DESCRIPTION = "Query all five AI surfaces instead of the three default ones, adding claude and google_ai_overview. Costs more per call. Use it when the user wants complete cross-model coverage, is comparing surfaces against each other, or asks specifically about Claude or Google AI Overviews. Ignored when models is given.";
151
- 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.";
152
- var MODEL_PRICING = "Pricing is per selected AI surface. Omitting models queries the three default surfaces (chatgpt, perplexity, gemini); fullSweep: true queries all five, adding claude and google_ai_overview for more credits. Use the models field when the user names a surface or asks to reduce credit use, and call cite42_pricing for exact current rates.";
180
+ 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 $5 USD, estimate the cost in USD and ask the user before continuing. Quote Cite42 costs and balances in USD using cite42Billing or the USD pricing table. Legacy credit fields are for compatibility only. These funds belong to Cite42, not the host or model provider. AI visibility metrics include counts; quote them with percentages, not as a market-wide score. Optional recommendations are a separate paid call, never automatic. After a visibility result, offer recommendations using its requestId; run only when the user requests advice and knows the additional cost. Free: cite42_balance for the remaining balance, cite42_pricing for exact per-tool prices and cost estimates, cite42_usage for recent calls.";
181
+ var MODEL_PRICING = "Pricing is per selected AI surface. Omitting models queries the three default surfaces (chatgpt, perplexity, gemini); fullSweep: true queries all five, adding claude and google_ai_overview at a higher cost. Use the models field when the user names a surface or asks to reduce spending, and call cite42_pricing for exact current rates.";
153
182
  var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
154
183
  var KEYWORDS_PRICING = "Pricing: billed by outcome \u2014 cheapest when the seed has no search volume, more when it has metrics, most with ideas:true. Call cite42_pricing for exact current rates.";
155
184
  var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the tools that run successfully and the selected model rates.";
@@ -176,16 +205,59 @@ function queryText(description) {
176
205
  function shortText(description) {
177
206
  return z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).describe(description);
178
207
  }
208
+ var aivRecommendations = createTool({
209
+ id: "cite42_recommendations",
210
+ mcp: readOnly("Recommend AI visibility next steps"),
211
+ description: "Optional evidence-backed AI visibility recommendations. Flat price from cite42_pricing per completed AI report; insufficient evidence and rule-only fallback are free. Supports rankings, compare, citations and sentiment, including usable workflow steps. No crawling, fresh research, keyword advice or tracker activation. " + MCP_USAGE_GUIDANCE,
212
+ inputSchema: z.object({
213
+ requestId: z.string().trim().min(1).max(128).optional().describe("A requestId from your own successful Cite42 call within the last 30 days."),
214
+ trackerRunId: z.string().trim().min(1).max(128).optional().describe("A specific finished run of your own tracker within the last 30 days."),
215
+ evidence: z.array(z.object({
216
+ primitive: z.enum(["rankings", "compare", "citations", "sentiment"]),
217
+ input: z.record(z.string(), z.unknown()),
218
+ result: z.record(z.string(), z.unknown()),
219
+ measuredAt: z.string().datetime()
220
+ })).min(1).max(20).optional(),
221
+ context: z.object({
222
+ brand: z.string().trim().min(1).max(200).optional(),
223
+ domain: z.string().trim().min(1).max(250).optional(),
224
+ audience: z.string().trim().min(1).max(400).optional(),
225
+ goal: z.string().trim().min(1).max(400).optional()
226
+ }).optional()
227
+ }).superRefine((input, ctx) => {
228
+ if ([input.requestId, input.trackerRunId, input.evidence].filter((value) => value !== void 0).length !== 1) {
229
+ ctx.addIssue({ code: "custom", message: "Provide exactly one of requestId, trackerRunId or evidence." });
230
+ }
231
+ if (new TextEncoder().encode(JSON.stringify(input)).length > 64 * 1024) {
232
+ ctx.addIssue({ code: "custom", message: "Recommendation input must fit within 64KB." });
233
+ }
234
+ }),
235
+ execute: async (input) => aivPost("/recommendations", input)
236
+ });
179
237
  function optionalShortText(description) {
180
238
  return z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).optional().describe(description);
181
239
  }
182
240
  var modelsInput = z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
183
241
  var fullSweepInput = z.boolean().optional().describe(FULL_SWEEP_DESCRIPTION);
184
242
  var competitorList = z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
243
+ var aivBalance = createTool({
244
+ id: "cite42_balance",
245
+ mcp: readOnly("Check Cite42 balance", false),
246
+ description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
247
+ inputSchema: z.object({}),
248
+ execute: async () => {
249
+ const usage = await aivGet("/usage");
250
+ const microCredits = usage.balance?.microCredits;
251
+ if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
252
+ throw new Error("Cite42 usage response did not include a balance.");
253
+ }
254
+ return toCite42Balance(microCredits);
255
+ }
256
+ });
185
257
  var aivCredits = createTool({
186
258
  id: "cite42_credits",
187
- mcp: readOnly("Check Cite42 credit balance", false),
188
- description: "Check the remaining Cite42 credit balance for the connected account. Free: costs 0 credits. " + MCP_USAGE_GUIDANCE,
259
+ mcp: readOnly("Check Cite42 balance (legacy alias)", false),
260
+ description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
189
261
  inputSchema: z.object({}),
190
262
  execute: async () => {
191
263
  const usage = await aivGet("/usage");
@@ -199,14 +271,16 @@ var aivCredits = createTool({
199
271
  var aivPricing = createTool({
200
272
  id: "cite42_pricing",
201
273
  mcp: readOnly("Get Cite42 price table", false),
202
- 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,
203
- inputSchema: z.object({}),
204
- execute: async () => aivGet("/pricing")
274
+ description: 'Get the exact Cite42 price table. Pass currency: USD for dollar prices; otherwise read the usd block (legacy fields are compatibility only): 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. ' + MCP_USAGE_GUIDANCE,
275
+ inputSchema: z.object({
276
+ currency: z.enum(["USD", "credits"]).optional().describe("Use USD for dollar prices. Omit only for legacy compatibility.")
277
+ }),
278
+ execute: async (input) => input.currency === "USD" ? aivGet("/pricing?currency=USD") : { ...await aivGet("/pricing"), usd: await aivGet("/pricing?currency=USD") }
205
279
  });
206
280
  var aivUsage = createTool({
207
281
  id: "cite42_usage",
208
282
  mcp: readOnly("Get recent Cite42 usage", false),
209
- 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,
283
+ description: "Get recent Cite42 usage for the connected account: the last calls with their cost and status, plus the remaining balance in USD. Free. " + MCP_USAGE_GUIDANCE,
210
284
  inputSchema: z.object({}),
211
285
  execute: async () => {
212
286
  const usage = await aivGet("/usage");
@@ -220,6 +294,7 @@ var aivUsage = createTool({
220
294
  endpoint: row.endpoint,
221
295
  status: row.status,
222
296
  credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
297
+ costUsd: typeof row.costMicro === "number" ? toUsd(row.costMicro) : void 0,
223
298
  at: row.createdAt
224
299
  }))
225
300
  };
@@ -301,6 +376,23 @@ var aivKeywords = createTool({
301
376
  }),
302
377
  execute: async (input) => aivPost("/keywords", input)
303
378
  });
379
+ var aivKeywordsBatch = createTool({
380
+ id: "cite42_keywords_batch",
381
+ mcp: readOnly("Get batch SEO keyword data"),
382
+ description: `Check search volume, CPC, and competition for two to 50 distinct seed keywords in one request. No related ideas. Billed by outcome; call cite42_pricing for current rates. ${MCP_USAGE_GUIDANCE}`,
383
+ inputSchema: z.object({
384
+ seeds: z.array(z.string().trim().min(1).max(80).refine(
385
+ (seed) => seed.split(/\s+/).length <= 10,
386
+ "Each keyword must contain at most 10 words."
387
+ )).min(2).max(50).refine(
388
+ (seeds) => new Set(seeds.map((seed) => seed.toLowerCase().replace(/\s+/g, " "))).size === seeds.length,
389
+ "Keywords must be distinct."
390
+ ).describe("Two to 50 distinct seed keywords."),
391
+ country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
392
+ language: z.string().trim().max(8).optional().describe("Optional ISO-2 language code.")
393
+ }),
394
+ execute: async (input) => aivPost("/keywords/batch", input)
395
+ });
304
396
  var aivTrends = createTool({
305
397
  id: "cite42_trends",
306
398
  mcp: readOnly("Get Google Trends data"),
@@ -314,14 +406,14 @@ var aivTrends = createTool({
314
406
  });
315
407
  var aivRedditTrends = createTool({
316
408
  id: "cite42_reddit_trends",
317
- mcp: readOnly("Get Reddit trend data"),
318
- 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}`,
409
+ mcp: readOnly("Find Reddit discussions"),
410
+ description: `Discover relevant Reddit discussions through Gemini Google Search. Returns labeled model summaries, citations and coverage warnings, with no author quotes or quote-based insights. Does not measure growth or read complete comment conversations. ${DATA_PRICING} ${MCP_USAGE_GUIDANCE}`,
319
411
  inputSchema: z.object({
320
412
  topic: queryText("Topic, category, product, or audience to research on Reddit."),
321
413
  subreddits: z.array(shortText("Subreddit name without r/.")).max(20).optional().describe("Optional subreddit names, without r/."),
322
414
  timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Conversation window, e.g. "past_7_days", "past_30_days", "past_90_days".')
323
415
  }),
324
- execute: async (input) => aivPost("/reddit/trends", input)
416
+ execute: async (input, context) => aivPost("/reddit/trends", input, context?.abortSignal ?? context?.mcp?.extra.signal)
325
417
  });
326
418
  var aivYoutubeTrends = createTool({
327
419
  id: "cite42_youtube_trends",
@@ -349,7 +441,7 @@ var aivFindContentOpportunities = createTool({
349
441
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
350
442
  channels: z.array(channelEnum).optional().describe("Optional data sources to include.")
351
443
  }),
352
- execute: async (input) => aivPost("/workflows/content-opportunities", input)
444
+ execute: async (input, context) => aivPost("/workflows/content-opportunities", input, context?.abortSignal ?? context?.mcp?.extra.signal)
353
445
  });
354
446
  var aivAnalyzeTopicDemand = createTool({
355
447
  id: "cite42_analyze_topic_demand",
@@ -362,13 +454,13 @@ var aivAnalyzeTopicDemand = createTool({
362
454
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
363
455
  timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
364
456
  }),
365
- execute: async (input) => aivPost("/workflows/topic-demand", input)
457
+ execute: async (input, context) => aivPost("/workflows/topic-demand", input, context?.abortSignal ?? context?.mcp?.extra.signal)
366
458
  });
367
459
  var aivMapAiPrompts = createTool({
368
460
  id: "cite42_map_ai_prompts",
369
461
  mcp: readOnly("Map buyer prompts for AI search"),
370
462
  description: workflowDescription(
371
- "Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data. The prompts it returns are exactly what cite42_tracker_create takes, so offer to start tracking the best of them once the user has seen the map."
463
+ "Run the AI-prompt-map workflow: research for a list of buyer prompts worth tracking, from AI search, SEO demand, trends, citations, Reddit, and YouTube data. It returns evidence and a suggested prompt, not a finished list: write the prompts from it, then offer to track the best of them with cite42_tracker_create once the user has seen them."
372
464
  ),
373
465
  inputSchema: z.object({
374
466
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
@@ -378,7 +470,7 @@ var aivMapAiPrompts = createTool({
378
470
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
379
471
  timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
380
472
  }),
381
- execute: async (input) => aivPost("/workflows/ai-prompt-map", input)
473
+ execute: async (input, context) => aivPost("/workflows/ai-prompt-map", input, context?.abortSignal ?? context?.mcp?.extra.signal)
382
474
  });
383
475
  var aivAnalyzeCompetitorContent = createTool({
384
476
  id: "cite42_analyze_competitor_content",
@@ -392,7 +484,7 @@ var aivAnalyzeCompetitorContent = createTool({
392
484
  brand: optionalShortText("Optional primary brand/domain to compare against."),
393
485
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
394
486
  }),
395
- execute: async (input) => aivPost("/workflows/competitor-content", input)
487
+ execute: async (input, context) => aivPost("/workflows/competitor-content", input, context?.abortSignal ?? context?.mcp?.extra.signal)
396
488
  });
397
489
  var aivFindAiCitationGaps = createTool({
398
490
  id: "cite42_find_ai_citation_gaps",
@@ -408,13 +500,13 @@ var aivFindAiCitationGaps = createTool({
408
500
  models: modelsInput,
409
501
  fullSweep: fullSweepInput
410
502
  }),
411
- execute: async (input) => aivPost("/workflows/ai-citation-gaps", input)
503
+ execute: async (input, context) => aivPost("/workflows/ai-citation-gaps", input, context?.abortSignal ?? context?.mcp?.extra.signal)
412
504
  });
413
505
  var aivGenerateContentBrief = createTool({
414
506
  id: "cite42_generate_content_brief",
415
507
  mcp: readOnly("Generate content brief data"),
416
508
  description: workflowDescription(
417
- "Run the content-brief workflow: AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals aggregated into brief-ready data."
509
+ "Run the content-brief workflow: research for a content brief from AI answers, citations, SEO keyword demand, trends, Reddit, and YouTube signals. It returns evidence and a suggested prompt; write the brief from it."
418
510
  ),
419
511
  inputSchema: z.object({
420
512
  topic: queryText("Topic, keyword, or content idea to brief."),
@@ -423,7 +515,7 @@ var aivGenerateContentBrief = createTool({
423
515
  goal: optionalShortText("Optional content goal, e.g. rank, earn citations, convert."),
424
516
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
425
517
  }),
426
- execute: async (input) => aivPost("/workflows/content-brief", input)
518
+ execute: async (input, context) => aivPost("/workflows/content-brief", input, context?.abortSignal ?? context?.mcp?.extra.signal)
427
519
  });
428
520
  var trackerIdInput = z.string().trim().min(1).max(128).describe("Cite42 tracker ID returned by a tracker management or report tool.");
429
521
  var trackerCadenceInput = z.object({
@@ -477,11 +569,11 @@ var trackerDefinitionFields = {
477
569
  "AI surfaces to query for every prompt. Omit for the three defaults (chatgpt, perplexity, gemini), or list surfaces explicitly \u2014 including all five \u2014 to fix the recurring cost."
478
570
  )
479
571
  };
480
- var TRACKER_BILLING_DESCRIPTION = "Tracker management and stored-report reads are free. Each scheduled or manual run bills one call per prompt at the normal per-model rates; the activation response and tracker record expose the accepted maximum cost per run.";
572
+ var TRACKER_BILLING_DESCRIPTION = "Tracker management and stored-report reads are free. Each scheduled or manual run bills one call per prompt at the normal per-model rates; the activation response and tracker record expose the accepted maximum cost per run. Present the returned USD amounts to users; activation still takes the exact estimatedCostMicroPerRun integer returned by the tracker.";
481
573
  var aivTrackerCreate = createTool({
482
574
  id: "cite42_tracker_create",
483
575
  mcp: mutation("Create Cite42 tracker draft"),
484
- description: "Create a draft weekly or monthly tracker: one measurement \u2014 rankings, compare, citations, or sentiment \u2014 applied to a list of prompts, with the brand, competitors and AI surfaces set once for all of them. When the user has no prompt list yet, run cite42_map_ai_prompts first and track the prompts it recommends. Creation does not start recurring calls. " + TRACKER_BILLING_DESCRIPTION,
576
+ description: "Create a draft weekly or monthly tracker: one measurement \u2014 rankings, compare, citations, or sentiment \u2014 applied to a list of prompts, with the brand, competitors and AI surfaces set once for all of them. When the user has no prompt list yet, run cite42_map_ai_prompts first and track the prompts you write from its research. Creation does not start recurring calls. " + TRACKER_BILLING_DESCRIPTION,
485
577
  inputSchema: z.object(trackerDefinitionFields),
486
578
  execute: async (input) => aivPost("/trackers", input)
487
579
  });
@@ -508,7 +600,7 @@ var aivTrackerActivate = createTool({
508
600
  var aivTrackerPause = createTool({
509
601
  id: "cite42_tracker_pause",
510
602
  mcp: mutation("Pause Cite42 tracker", { idempotentHint: true }),
511
- description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free: costs 0 credits.",
603
+ description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free.",
512
604
  inputSchema: z.object({ trackerId: trackerIdInput }),
513
605
  execute: async ({ trackerId }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/pause`, {})
514
606
  });
@@ -518,7 +610,7 @@ var aivTrackerDelete = createTool({
518
610
  destructiveHint: true,
519
611
  idempotentHint: true
520
612
  }),
521
- 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.",
613
+ 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 billing ledger records are retained. Free.",
522
614
  inputSchema: z.object({ trackerId: trackerIdInput }),
523
615
  execute: async ({ trackerId }) => aivDelete(`/trackers/${encodeURIComponent(trackerId)}`)
524
616
  });
@@ -538,14 +630,14 @@ var aivTrackerRun = createTool({
538
630
  var aivTrackers = createTool({
539
631
  id: "cite42_trackers",
540
632
  mcp: readOnly("List Cite42 trackers", false),
541
- description: "List the connected account\u2019s trackers with status, cadence, tool configuration, cost estimate, accepted per-run maximum, and next/last run times. Free: costs 0 credits.",
633
+ description: "List the connected account\u2019s trackers with status, cadence, tool configuration, cost estimate, accepted per-run maximum, and next/last run times. Free.",
542
634
  inputSchema: z.object({}),
543
635
  execute: async () => aivGet("/trackers")
544
636
  });
545
637
  var aivTrackerReport = createTool({
546
638
  id: "cite42_tracker_report",
547
639
  mcp: readOnly("Read Cite42 tracker report", false),
548
- description: "Read bounded stored run history for one tracker, including per-tool results, changes from the prior comparable observation, statuses, sampled times, and costs. Free: costs 0 credits.",
640
+ description: "Read bounded stored run history for one tracker, including per-tool results, changes from the prior comparable observation, statuses, sampled times, and costs. Free.",
549
641
  inputSchema: z.object({
550
642
  trackerId: trackerIdInput,
551
643
  limit: z.number().int().min(1).max(20).optional().default(8).describe("Most recent runs to return, capped for LLM context.")
@@ -555,6 +647,7 @@ var aivTrackerReport = createTool({
555
647
 
556
648
  // src/server.ts
557
649
  var CITE42_TOOLS = {
650
+ cite42_balance: aivBalance,
558
651
  cite42_credits: aivCredits,
559
652
  cite42_pricing: aivPricing,
560
653
  cite42_usage: aivUsage,
@@ -563,7 +656,9 @@ var CITE42_TOOLS = {
563
656
  cite42_compare: aivCompare,
564
657
  cite42_citations: aivCitations,
565
658
  cite42_sentiment: aivSentiment,
659
+ cite42_recommendations: aivRecommendations,
566
660
  cite42_keywords: aivKeywords,
661
+ cite42_keywords_batch: aivKeywordsBatch,
567
662
  cite42_trends: aivTrends,
568
663
  cite42_reddit_trends: aivRedditTrends,
569
664
  cite42_youtube_trends: aivYoutubeTrends,
@@ -599,7 +694,7 @@ if (!resolveApiKey()) {
599
694
  );
600
695
  process.exit(1);
601
696
  }
602
- buildCite42Server("0.5.1").startStdio().catch((error) => {
697
+ buildCite42Server("0.6.1").startStdio().catch((error) => {
603
698
  console.error("[@cite42/mcp] Fatal error:", error);
604
699
  process.exit(1);
605
700
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cite42/mcp",
3
3
  "mcpName": "dev.cite42/mcp",
4
- "version": "0.5.1",
4
+ "version": "0.6.1",
5
5
  "description": "Cite42 MCP server: query the models behind ChatGPT, Claude, Perplexity & Gemini, plus Google AI Overviews, for brand rankings, AI citations, SEO keyword data, and Reddit/YouTube trends, and schedule weekly or monthly trackers that watch a list of prompts over time. Works with Claude Code, Claude Desktop, Codex CLI, Cursor, and any MCP client.",
6
6
  "license": "MIT",
7
7
  "type": "module",
@@ -16,21 +16,24 @@
16
16
  "scripts": {
17
17
  "build": "tsup && chmod +x dist/stdio.mjs",
18
18
  "dev": "tsup --watch",
19
+ "inspect": "npm run build && node scripts/inspect.mjs",
20
+ "test:stdio": "npm run build && node --test tests/stdio.test.mjs",
19
21
  "test": "vitest run",
20
22
  "test:watch": "vitest",
21
- "prepublishOnly": "npm run build"
23
+ "prepublishOnly": "npm run build && npm run audit:security",
24
+ "audit:security": "npm audit --omit=dev --audit-level=high"
22
25
  },
23
26
  "dependencies": {
24
- "@mastra/core": "^1.35.0",
25
- "@mastra/mcp": "^1.7.0",
27
+ "@mastra/core": "^1.68.0",
28
+ "@mastra/mcp": "^1.18.0",
26
29
  "zod": "^4.4.3"
27
30
  },
28
31
  "devDependencies": {
29
32
  "@modelcontextprotocol/sdk": "^1.29.0",
30
- "@types/node": "^20.19.27",
33
+ "@types/node": "^22.19.0",
31
34
  "tsup": "^8.0.0",
32
35
  "typescript": "^5.9.3",
33
- "vitest": "^4.1.10"
36
+ "vitest": "^4.1.11"
34
37
  },
35
38
  "publishConfig": {
36
39
  "access": "public"
@@ -66,12 +69,15 @@
66
69
  "scheduled-monitoring"
67
70
  ],
68
71
  "engines": {
69
- "node": ">=18"
72
+ "node": ">=22.13.0"
70
73
  },
71
74
  "author": "Cite42 <support@cite42.dev> (https://www.cite42.dev)",
72
75
  "homepage": "https://www.cite42.dev",
73
76
  "bugs": {
74
77
  "email": "support@cite42.dev",
75
78
  "url": "https://www.cite42.dev/app/support"
79
+ },
80
+ "overrides": {
81
+ "esbuild": "^0.28.2"
76
82
  }
77
83
  }