@cite42/mcp 0.7.0 → 0.8.0

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
@@ -138,7 +138,7 @@ Cite42 exposes 29 MCP tools: 4 free account tools (including the compatibility a
138
138
  ### Account & pricing (free)
139
139
 
140
140
  - `cite42_balance`: check the connected account’s remaining Cite42 balance in USD.
141
- - `cite42_credits`: compatibility alias for older integrations.
141
+ - `cite42_credits`: alias of `cite42_balance` for older integrations.
142
142
  - `cite42_pricing`: get current per-tool prices and estimate a planned set of calls.
143
143
  - `cite42_usage`: read recent call status and cost together with the remaining balance.
144
144
 
@@ -156,7 +156,7 @@ Cite42 exposes 29 MCP tools: 4 free account tools (including the compatibility a
156
156
  - `cite42_keywords`: get seed search volume, CPC, and competition, with optional related keyword ideas.
157
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).
158
158
  - `cite42_trends`: get Google Trends interest over time, related and rising queries, and a trend label.
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.
159
+ - `cite42_reddit_trends`: find relevant Reddit discussions with cited summaries and read up to three pages through Gemini URL context. Returns page titles, authors, post excerpts, relative ages, available scores/comment counts and sampled human comments. Pages may be cached and model extraction is not independently verified. Exact dates, growth and quote-based insight arrays remain unavailable. Read `relevantThreads`; `risingThreads` is a deprecated compatibility alias. Check `coverage` and `warnings` for missing fields and out-of-window threads.
160
160
  - `cite42_youtube_trends`: find YouTube rising videos, creator angles, title patterns, and opportunities.
161
161
 
162
162
  ### Research workflows (multi-source, one call)
@@ -213,17 +213,15 @@ smoke test against a loopback fake API. See [local MCP testing](../../docs/mcp-t
213
213
  - Docs: [Cite42 API & MCP documentation](https://www.cite42.dev/docs)
214
214
  - Pricing: [Cite42 pay-per-call pricing](https://www.cite42.dev/pricing)
215
215
 
216
- ### USD billing and older clients
216
+ ### USD billing
217
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.
218
+ Every paid result carries a `cite42Billing` block with the amount charged and the remaining
219
+ Cite42 balance. Its `chargedUsd` and `remainingUsd` values are decimal strings in USD.
220
+ Credits are retired: since 0.8.0 the package no longer returns `cite42Credits`, and
221
+ `cite42_credits` answers exactly like `cite42_balance`.
221
222
 
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
223
+ `cite42_pricing` always returns the USD price table; an older `currency` argument is ignored.
224
+ REST clients can use `GET /v1/pricing?currency=USD`. Tracker activation still accepts the exact
227
225
  integer `maxCostMicroPerRun` returned by the API; present its USD equivalent to users.
228
226
 
229
227
  ## Optional AI visibility recommendations
package/dist/stdio.cjs CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/client.ts
5
5
  var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
6
- var MICRO_CREDITS_PER_CREDIT = 1e3;
6
+ var FUNDS_LOCATION = "the Cite42 dashboard at https://www.cite42.dev/app";
7
7
  function toUsd(micro) {
8
8
  const [whole, fraction = ""] = (micro / 1e5).toFixed(5).split(".");
9
9
  return `${whole}.${fraction.slice(0, Math.max(2, fraction.replace(/0+$/, "").length))}`;
@@ -20,16 +20,6 @@ function toCite42Balance(micro) {
20
20
  function isJsonObject(value) {
21
21
  return typeof value === "object" && value !== null && !Array.isArray(value);
22
22
  }
23
- function toCredits(microCredits) {
24
- return microCredits / MICRO_CREDITS_PER_CREDIT;
25
- }
26
- function formatCredits(microCredits) {
27
- const credits = toCredits(microCredits);
28
- return credits.toLocaleString(
29
- "en-US",
30
- Number.isInteger(credits) ? { maximumFractionDigits: 0 } : { minimumFractionDigits: 0, maximumFractionDigits: 2 }
31
- );
32
- }
33
23
  function isTrackerCommitment(billing) {
34
24
  return typeof billing.balanceMicro === "number" && typeof billing.costMicroPerRun === "number";
35
25
  }
@@ -53,38 +43,17 @@ function toCite42McpResult(value) {
53
43
  ...replay ? { originalChargedUsd: toUsd(originalCost) } : {},
54
44
  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
45
  };
56
- const lowCredits = typeof warning === "string";
57
- const message = `Cite42 credits: ${formatCredits(cost)} charged; ${formatCredits(balance)} remaining.`;
58
- return {
59
- ...result,
60
- cite42Billing,
61
- // Legacy numeric fields retain their original units for existing integrations.
62
- cite42Credits: {
63
- charged: toCredits(cost),
64
- remaining: toCredits(balance),
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
67
- }
68
- };
69
- }
70
- function toCite42CreditsBalance(balanceMicroCredits) {
71
- return {
72
- ...toCite42Balance(balanceMicroCredits),
73
- cite42Credits: {
74
- remaining: toCredits(balanceMicroCredits),
75
- message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
76
- }
77
- };
46
+ return { ...result, cite42Billing };
78
47
  }
79
48
  function errorMessageForMcp(status, path, body) {
80
49
  try {
81
50
  const payload = JSON.parse(body);
82
51
  if (isJsonObject(payload) && isJsonObject(payload.error) && typeof payload.error.code === "string") {
83
52
  if (payload.error.code === "daily_limit_exceeded" || payload.error.code === "monthly_limit_exceeded") {
84
- return "Cite42 spending limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
53
+ return `Cite42 spending limit reached. Change it in ${FUNDS_LOCATION}.`;
85
54
  }
86
55
  if (payload.error.code === "insufficient_credits") {
87
- return "Insufficient Cite42 balance. Add funds at https://www.cite42.dev/app/billing.";
56
+ return `Insufficient Cite42 balance. Add funds in ${FUNDS_LOCATION}.`;
88
57
  }
89
58
  }
90
59
  } catch {
@@ -176,18 +145,18 @@ var modelEnum = import_zod.z.enum(["chatgpt", "claude", "perplexity", "gemini",
176
145
  var QUERY_MAX_CHARS = 2e3;
177
146
  var SHORT_TEXT_MAX_CHARS = 200;
178
147
  var TIMEFRAME_MAX_CHARS = 80;
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.';
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.";
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. Recommendations take the requestId of a visibility result and the page URL, or a page URL alone for a free technical check; they run only when the user requests advice and knows any 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.";
183
- var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
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.";
185
- var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the tools that run successfully and the selected model rates.";
148
+ var MODEL_SELECTION_DESCRIPTION = "AI surfaces to query, each billed separately; pass a subset when the user names surfaces or wants a lower cost. Omit for the defaults (chatgpt, perplexity, gemini); fullSweep queries all five. google_ai_overview accepts queries up to 700 characters, the others 2000.";
149
+ var FULL_SWEEP_DESCRIPTION = "Query all five AI surfaces (adds claude and google_ai_overview) for full cross-surface coverage, at a higher cost. Ignored when models is given.";
150
+ var PAID_TOOL_BILLING = "Billed in USD (result field cite42Billing) to the user's prepaid Cite42 balance; exact prices from cite42_pricing (free). For plans over 20 calls or $5, estimate the cost and confirm with the user first.";
151
+ var RECOMMENDATIONS_INPUT_NOTE = "Its requestId can be passed to cite42_recommendations when the user asks for improvement advice.";
152
+ var MODEL_PRICING = "Priced per AI surface queried.";
153
+ var KEYWORDS_PRICING = "Billed by outcome: cheapest when the seed has no search volume, more when it has metrics, most with ideas:true.";
154
+ var WORKFLOW_PRICING = "Billed for the steps that succeed, at per-surface rates.";
186
155
  function workflowDescription(description) {
187
- return `${description} ${MCP_USAGE_GUIDANCE} ${MODEL_PRICING} ${DATA_PRICING} ${WORKFLOW_PRICING}`;
156
+ return `${description} ${WORKFLOW_PRICING} ${PAID_TOOL_BILLING}`;
188
157
  }
189
158
  function readOnly(title, openWorldHint = true) {
190
- return { annotations: { title, readOnlyHint: true, openWorldHint } };
159
+ return { annotations: { title, readOnlyHint: true, destructiveHint: false, openWorldHint } };
191
160
  }
192
161
  function mutation(title, options = {}) {
193
162
  return {
@@ -209,7 +178,7 @@ function shortText(description) {
209
178
  var aivRecommendations = (0, import_tools.createTool)({
210
179
  id: "cite42_recommendations",
211
180
  mcp: readOnly("Recommend AI visibility next steps"),
212
- description: "Page-level AI visibility recommendations. With a saved result (requestId, trackerRunId or evidence) it reports on one measured prompt, charged only when the report includes page-comparison advice backed by quotes from inspected pages. Without one: url alone runs a free technical check of the page (robots.txt crawler access, indexing, rendering) and returns suggested buyer questions; url with query first runs a citations check, priced like cite42_citations, then the report. Prices are in cite42_pricing. Fetches the page, its robots.txt and up to two pages the AI surfaces cited, as returned HTML without JavaScript. No site-wide crawl, keyword advice or tracker activation. " + MCP_USAGE_GUIDANCE,
181
+ description: "Page-level AI visibility recommendations, for when the user asks how to improve a page. With a saved result (requestId, trackerRunId or evidence) it reports on one measured prompt, charged only when the report includes page-comparison advice backed by quotes from inspected pages. Without one: url alone runs a free technical check of the page (robots.txt crawler access, indexing, rendering) and returns suggested buyer questions; url with query first runs a citations check, priced like cite42_citations, then the report. Prices are in cite42_pricing. Fetches the page, its robots.txt and up to two pages the AI surfaces cited, as returned HTML without JavaScript. No site-wide crawl, keyword advice or tracker activation. " + PAID_TOOL_BILLING,
213
182
  inputSchema: import_zod.z.object({
214
183
  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
184
  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."),
@@ -249,60 +218,52 @@ function optionalShortText(description) {
249
218
  var modelsInput = import_zod.z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
250
219
  var fullSweepInput = import_zod.z.boolean().optional().describe(FULL_SWEEP_DESCRIPTION);
251
220
  var competitorList = import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
221
+ async function fetchBalance() {
222
+ const usage = await aivGet("/usage");
223
+ const micro = usage.balance?.microCredits;
224
+ if (typeof micro !== "number" || !Number.isFinite(micro)) {
225
+ throw new Error("Cite42 usage response did not include a balance.");
226
+ }
227
+ return toCite42Balance(micro);
228
+ }
252
229
  var aivBalance = (0, import_tools.createTool)({
253
230
  id: "cite42_balance",
254
231
  mcp: readOnly("Check Cite42 balance", false),
255
- description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
232
+ description: "Check the remaining Cite42 balance in USD for the connected account. Free.",
256
233
  inputSchema: import_zod.z.object({}),
257
- execute: async () => {
258
- const usage = await aivGet("/usage");
259
- const microCredits = usage.balance?.microCredits;
260
- if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
261
- throw new Error("Cite42 usage response did not include a balance.");
262
- }
263
- return toCite42Balance(microCredits);
264
- }
234
+ execute: fetchBalance
265
235
  });
266
236
  var aivCredits = (0, import_tools.createTool)({
267
237
  id: "cite42_credits",
268
238
  mcp: readOnly("Check Cite42 balance (legacy alias)", false),
269
- description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
239
+ description: "Alias of cite42_balance for older integrations. Free.",
270
240
  inputSchema: import_zod.z.object({}),
271
- execute: async () => {
272
- const usage = await aivGet("/usage");
273
- const microCredits = usage.balance?.microCredits;
274
- if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
275
- throw new Error("Cite42 usage response did not include a balance.");
276
- }
277
- return toCite42CreditsBalance(microCredits);
278
- }
241
+ execute: fetchBalance
279
242
  });
280
243
  var aivPricing = (0, import_tools.createTool)({
281
244
  id: "cite42_pricing",
282
245
  mcp: readOnly("Get Cite42 price table", false),
283
- 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,
284
- inputSchema: import_zod.z.object({
285
- currency: import_zod.z.enum(["USD", "credits"]).optional().describe("Use USD for dollar prices. Omit only for legacy compatibility.")
286
- }),
287
- execute: async (input) => input.currency === "USD" ? aivGet("/pricing?currency=USD") : { ...await aivGet("/pricing"), usd: await aivGet("/pricing?currency=USD") }
246
+ description: "Get the exact Cite42 price table in USD: per-surface rates, flat-rate tools, workflow maximums, and which tools are free. Use it for cost questions and to estimate a plan before running it. Free.",
247
+ // No currency switch: prices are USD only. An older client's `currency` argument is ignored.
248
+ inputSchema: import_zod.z.object({}),
249
+ execute: async () => aivGet("/pricing?currency=USD")
288
250
  });
289
251
  var aivUsage = (0, import_tools.createTool)({
290
252
  id: "cite42_usage",
291
253
  mcp: readOnly("Get recent Cite42 usage", false),
292
- 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,
254
+ description: "Get recent Cite42 usage for the connected account: the last calls with their cost and status, plus the remaining balance in USD. Free.",
293
255
  inputSchema: import_zod.z.object({}),
294
256
  execute: async () => {
295
257
  const usage = await aivGet("/usage");
296
- const microCredits = usage.balance?.microCredits;
297
- if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
258
+ const micro = usage.balance?.microCredits;
259
+ if (typeof micro !== "number" || !Number.isFinite(micro)) {
298
260
  throw new Error("Cite42 usage response did not include a balance.");
299
261
  }
300
262
  return {
301
- ...toCite42CreditsBalance(microCredits),
263
+ ...toCite42Balance(micro),
302
264
  recentCalls: (usage.recent ?? []).slice(0, 20).map((row) => ({
303
265
  endpoint: row.endpoint,
304
266
  status: row.status,
305
- credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
306
267
  costUsd: typeof row.costMicro === "number" ? toUsd(row.costMicro) : void 0,
307
268
  at: row.createdAt
308
269
  }))
@@ -312,7 +273,7 @@ var aivUsage = (0, import_tools.createTool)({
312
273
  var aivSearch = (0, import_tools.createTool)({
313
274
  id: "cite42_search",
314
275
  mcp: readOnly("Search AI models"),
315
- 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}`,
276
+ 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} ${PAID_TOOL_BILLING}`,
316
277
  inputSchema: import_zod.z.object({
317
278
  query: queryText("The concise AI-search/market-research query to run against the selected models."),
318
279
  models: modelsInput,
@@ -323,7 +284,7 @@ var aivSearch = (0, import_tools.createTool)({
323
284
  var aivCitations = (0, import_tools.createTool)({
324
285
  id: "cite42_citations",
325
286
  mcp: readOnly("Find AI citations"),
326
- description: `Find which sources a model's web search surfaces for a query. Aggregates cited URLs across models with per-model counts; optionally checks whether a specific URL is cited. Measures the retrieval corpus behind a prompt \u2014 which domains own your category in AI search, and where your content is missing \u2014 not the citation links a consumer chat UI renders for a logged-in user. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
287
+ description: `Find which sources a model's web search surfaces for a query. Aggregates cited URLs across models with per-model counts; optionally checks whether a specific URL is cited. Measures the retrieval corpus behind a prompt \u2014 which domains own your category in AI search, and where your content is missing \u2014 not the citation links a consumer chat UI renders for a logged-in user. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
327
288
  inputSchema: import_zod.z.object({
328
289
  query: queryText("The concise query to look up cited sources for."),
329
290
  url: import_zod.z.string().trim().max(500).optional().describe("Optional URL to check whether it was cited."),
@@ -335,7 +296,7 @@ var aivCitations = (0, import_tools.createTool)({
335
296
  var aivRankings = (0, import_tools.createTool)({
336
297
  id: "cite42_rankings",
337
298
  mcp: readOnly("Measure AI brand rankings"),
338
- 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}`,
299
+ 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. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
339
300
  inputSchema: import_zod.z.object({
340
301
  query: queryText("The concise query to measure brand/competitor visibility on."),
341
302
  competitors: competitorList.describe("Brand or domain names to track within the AI-generated answers."),
@@ -347,7 +308,7 @@ var aivRankings = (0, import_tools.createTool)({
347
308
  var aivCompare = (0, import_tools.createTool)({
348
309
  id: "cite42_compare",
349
310
  mcp: readOnly("Compare brand against competitors"),
350
- 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}`,
311
+ 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. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
351
312
  inputSchema: import_zod.z.object({
352
313
  query: queryText("The concise query to compare the brand and competitors on."),
353
314
  brand: shortText("Your brand, ranked alongside the competitors."),
@@ -360,7 +321,7 @@ var aivCompare = (0, import_tools.createTool)({
360
321
  var aivSentiment = (0, import_tools.createTool)({
361
322
  id: "cite42_sentiment",
362
323
  mcp: readOnly("Score brand sentiment in AI answers"),
363
- 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}`,
324
+ 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. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
364
325
  inputSchema: import_zod.z.object({
365
326
  query: queryText("The concise query whose answers are scored for brand sentiment."),
366
327
  brand: shortText("The brand to score sentiment for in each model answer."),
@@ -372,7 +333,7 @@ var aivSentiment = (0, import_tools.createTool)({
372
333
  var aivKeywords = (0, import_tools.createTool)({
373
334
  id: "cite42_keywords",
374
335
  mcp: readOnly("Get SEO keyword data"),
375
- 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. Not a multi-model AI-search query. The reply echoes requestedKeyword and carries status "exact" or "no_data"; when the seed has no search volume, any related term that does appears under alternatives \u2014 never treat an alternative's metrics as the requested keyword's. ${KEYWORDS_PRICING} ${MCP_USAGE_GUIDANCE}`,
336
+ 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. Not a multi-model AI-search query. The reply echoes requestedKeyword and carries status "exact" or "no_data"; when the seed has no search volume, any related term that does appears under alternatives \u2014 never treat an alternative's metrics as the requested keyword's. ${KEYWORDS_PRICING} ${PAID_TOOL_BILLING}`,
376
337
  inputSchema: import_zod.z.object({
377
338
  seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
378
339
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
@@ -388,7 +349,7 @@ var aivKeywords = (0, import_tools.createTool)({
388
349
  var aivKeywordsBatch = (0, import_tools.createTool)({
389
350
  id: "cite42_keywords_batch",
390
351
  mcp: readOnly("Get batch SEO keyword data"),
391
- 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}`,
352
+ description: `Check search volume, CPC, and competition for two to 50 distinct seed keywords in one request. No related ideas. Billed by outcome. ${PAID_TOOL_BILLING}`,
392
353
  inputSchema: import_zod.z.object({
393
354
  seeds: import_zod.z.array(import_zod.z.string().trim().min(1).max(80).refine(
394
355
  (seed) => seed.split(/\s+/).length <= 10,
@@ -405,7 +366,7 @@ var aivKeywordsBatch = (0, import_tools.createTool)({
405
366
  var aivTrends = (0, import_tools.createTool)({
406
367
  id: "cite42_trends",
407
368
  mcp: readOnly("Get Google Trends data"),
408
- 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}`,
369
+ 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. ${PAID_TOOL_BILLING}`,
409
370
  inputSchema: import_zod.z.object({
410
371
  term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
411
372
  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".'),
@@ -416,7 +377,7 @@ var aivTrends = (0, import_tools.createTool)({
416
377
  var aivRedditTrends = (0, import_tools.createTool)({
417
378
  id: "cite42_reddit_trends",
418
379
  mcp: readOnly("Find Reddit discussions"),
419
- 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}`,
380
+ description: "Discover relevant Reddit discussions with Google Search and read up to three thread pages through Gemini URL context. Returns Gemini-read titles, authors, post excerpts, relative ages, available scores/comment counts and sampled human comments, plus cited model summaries. Check coverage and warnings: pages may be cached, extraction is not independently verified, and exact dates, growth and quote-based insights are unavailable. risingThreads is a deprecated alias of relevantThreads. " + PAID_TOOL_BILLING,
420
381
  inputSchema: import_zod.z.object({
421
382
  topic: queryText("Topic, category, product, or audience to research on Reddit."),
422
383
  subreddits: import_zod.z.array(shortText("Subreddit name without r/.")).max(20).optional().describe("Optional subreddit names, without r/."),
@@ -427,7 +388,7 @@ var aivRedditTrends = (0, import_tools.createTool)({
427
388
  var aivYoutubeTrends = (0, import_tools.createTool)({
428
389
  id: "cite42_youtube_trends",
429
390
  mcp: readOnly("Get YouTube trend data"),
430
- 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}`,
391
+ description: `YouTube trend data for rising videos, creator angles, title patterns, channel examples, and video-market opportunities around a topic. ${PAID_TOOL_BILLING}`,
431
392
  inputSchema: import_zod.z.object({
432
393
  topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
433
394
  region: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country/region code. Defaults to "US".'),
@@ -448,7 +409,9 @@ var aivFindContentOpportunities = (0, import_tools.createTool)({
448
409
  brand: optionalShortText("Optional brand/domain to account for."),
449
410
  competitors: competitorList.optional().describe("Competitor brands/domains."),
450
411
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
451
- channels: import_zod.z.array(channelEnum).optional().describe("Optional data sources to include.")
412
+ channels: import_zod.z.array(channelEnum).optional().describe("Optional data sources to include."),
413
+ models: modelsInput,
414
+ fullSweep: fullSweepInput
452
415
  }),
453
416
  execute: async (input, context) => aivPost("/workflows/content-opportunities", input, context?.abortSignal ?? context?.mcp?.extra.signal)
454
417
  });
@@ -461,7 +424,9 @@ var aivAnalyzeTopicDemand = (0, import_tools.createTool)({
461
424
  inputSchema: import_zod.z.object({
462
425
  topic: queryText("Topic, keyword, category, or audience demand to analyze."),
463
426
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
464
- timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
427
+ timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".'),
428
+ models: modelsInput,
429
+ fullSweep: fullSweepInput
465
430
  }),
466
431
  execute: async (input, context) => aivPost("/workflows/topic-demand", input, context?.abortSignal ?? context?.mcp?.extra.signal)
467
432
  });
@@ -469,7 +434,7 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
469
434
  id: "cite42_map_ai_prompts",
470
435
  mcp: readOnly("Map buyer prompts for AI search"),
471
436
  description: workflowDescription(
472
- "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."
437
+ "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; prompts written from it can be tracked with cite42_tracker_create."
473
438
  ),
474
439
  inputSchema: import_zod.z.object({
475
440
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
@@ -477,7 +442,9 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
477
442
  brand: optionalShortText("Optional brand/domain to include in prompt tracking recommendations."),
478
443
  competitors: competitorList.optional().describe("Competitor brands/domains."),
479
444
  country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
480
- timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
445
+ timeframe: import_zod.z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".'),
446
+ models: modelsInput,
447
+ fullSweep: fullSweepInput
481
448
  }),
482
449
  execute: async (input, context) => aivPost("/workflows/ai-prompt-map", input, context?.abortSignal ?? context?.mcp?.extra.signal)
483
450
  });
@@ -491,7 +458,9 @@ var aivAnalyzeCompetitorContent = (0, import_tools.createTool)({
491
458
  competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
492
459
  topic: import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
493
460
  brand: optionalShortText("Optional primary brand/domain to compare against."),
494
- country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
461
+ country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
462
+ models: modelsInput,
463
+ fullSweep: fullSweepInput
495
464
  }),
496
465
  execute: async (input, context) => aivPost("/workflows/competitor-content", input, context?.abortSignal ?? context?.mcp?.extra.signal)
497
466
  });
@@ -522,7 +491,9 @@ var aivGenerateContentBrief = (0, import_tools.createTool)({
522
491
  audience: optionalShortText("Optional target audience or ICP."),
523
492
  brand: optionalShortText("Optional brand/domain the brief is for."),
524
493
  goal: optionalShortText("Optional content goal, e.g. rank, earn citations, convert."),
525
- country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
494
+ country: import_zod.z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
495
+ models: modelsInput,
496
+ fullSweep: fullSweepInput
526
497
  }),
527
498
  execute: async (input, context) => aivPost("/workflows/content-brief", input, context?.abortSignal ?? context?.mcp?.extra.signal)
528
499
  });
@@ -578,11 +549,11 @@ var trackerDefinitionFields = {
578
549
  "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."
579
550
  )
580
551
  };
581
- 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.";
552
+ var TRACKER_BILLING_DESCRIPTION = "Managing trackers and reading reports is free; each run bills one call per prompt at the per-surface rates.";
582
553
  var aivTrackerCreate = (0, import_tools.createTool)({
583
554
  id: "cite42_tracker_create",
584
555
  mcp: mutation("Create Cite42 tracker draft"),
585
- 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,
556
+ 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. Prompt lists can come from cite42_map_ai_prompts research. Creation does not start recurring calls. " + TRACKER_BILLING_DESCRIPTION,
586
557
  inputSchema: import_zod.z.object(trackerDefinitionFields),
587
558
  execute: async (input) => aivPost("/trackers", input)
588
559
  });
@@ -599,7 +570,7 @@ var aivTrackerActivate = (0, import_tools.createTool)({
599
570
  openWorldHint: true,
600
571
  idempotentHint: true
601
572
  }),
602
- description: `Activate or resume recurring weekly or monthly tool calls. maxCostMicroPerRun must equal the tracker estimate being accepted; activation authorizes future billed calls until the tracker is paused. The response carries a billing block with the balance, the cost per run, and runsDryAt \u2014 the date the balance stops covering the account's active trackers \u2014 so relay it. When tracker emails are enabled, activation sends a setup summary and every completed run sends current results and suggested next steps, even when nothing changed. Activation is refused when the balance cannot cover even the next run; tell the user to top up at cite42.dev/app/billing. ${TRACKER_BILLING_DESCRIPTION}`,
573
+ description: `Activate or resume recurring weekly or monthly tool calls. maxCostMicroPerRun must equal the tracker's latest estimatedCostMicroPerRun; activation authorizes future billed calls until paused. The response includes the balance, the cost per run, and runsDryAt, the date the balance stops covering active trackers. When tracker emails are enabled, activation sends a setup summary and every completed run sends current results and suggested next steps, even when nothing changed. Activation is refused when the balance cannot cover the next run. ${TRACKER_BILLING_DESCRIPTION}`,
603
574
  inputSchema: import_zod.z.object({
604
575
  trackerId: trackerIdInput,
605
576
  maxCostMicroPerRun: import_zod.z.number().int().nonnegative().describe("Exact estimatedCostMicroPerRun returned by the latest tracker record.")
@@ -703,7 +674,7 @@ if (!resolveApiKey()) {
703
674
  );
704
675
  process.exit(1);
705
676
  }
706
- buildCite42Server("0.7.0").startStdio().catch((error) => {
677
+ buildCite42Server("0.8.0").startStdio().catch((error) => {
707
678
  console.error("[@cite42/mcp] Fatal error:", error);
708
679
  process.exit(1);
709
680
  });
package/dist/stdio.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/client.ts
4
4
  var DEFAULT_BASE_URL = "https://www.cite42.dev/api/v1";
5
- var MICRO_CREDITS_PER_CREDIT = 1e3;
5
+ var FUNDS_LOCATION = "the Cite42 dashboard at https://www.cite42.dev/app";
6
6
  function toUsd(micro) {
7
7
  const [whole, fraction = ""] = (micro / 1e5).toFixed(5).split(".");
8
8
  return `${whole}.${fraction.slice(0, Math.max(2, fraction.replace(/0+$/, "").length))}`;
@@ -19,16 +19,6 @@ function toCite42Balance(micro) {
19
19
  function isJsonObject(value) {
20
20
  return typeof value === "object" && value !== null && !Array.isArray(value);
21
21
  }
22
- function toCredits(microCredits) {
23
- return microCredits / MICRO_CREDITS_PER_CREDIT;
24
- }
25
- function formatCredits(microCredits) {
26
- const credits = toCredits(microCredits);
27
- return credits.toLocaleString(
28
- "en-US",
29
- Number.isInteger(credits) ? { maximumFractionDigits: 0 } : { minimumFractionDigits: 0, maximumFractionDigits: 2 }
30
- );
31
- }
32
22
  function isTrackerCommitment(billing) {
33
23
  return typeof billing.balanceMicro === "number" && typeof billing.costMicroPerRun === "number";
34
24
  }
@@ -52,38 +42,17 @@ function toCite42McpResult(value) {
52
42
  ...replay ? { originalChargedUsd: toUsd(originalCost) } : {},
53
43
  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
44
  };
55
- const lowCredits = typeof warning === "string";
56
- const message = `Cite42 credits: ${formatCredits(cost)} charged; ${formatCredits(balance)} remaining.`;
57
- return {
58
- ...result,
59
- cite42Billing,
60
- // Legacy numeric fields retain their original units for existing integrations.
61
- cite42Credits: {
62
- charged: toCredits(cost),
63
- remaining: toCredits(balance),
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
66
- }
67
- };
68
- }
69
- function toCite42CreditsBalance(balanceMicroCredits) {
70
- return {
71
- ...toCite42Balance(balanceMicroCredits),
72
- cite42Credits: {
73
- remaining: toCredits(balanceMicroCredits),
74
- message: `Cite42 credits: ${formatCredits(balanceMicroCredits)} remaining. This balance check was free.`
75
- }
76
- };
45
+ return { ...result, cite42Billing };
77
46
  }
78
47
  function errorMessageForMcp(status, path, body) {
79
48
  try {
80
49
  const payload = JSON.parse(body);
81
50
  if (isJsonObject(payload) && isJsonObject(payload.error) && typeof payload.error.code === "string") {
82
51
  if (payload.error.code === "daily_limit_exceeded" || payload.error.code === "monthly_limit_exceeded") {
83
- return "Cite42 spending limit reached. Adjust your limit at https://www.cite42.dev/app/billing.";
52
+ return `Cite42 spending limit reached. Change it in ${FUNDS_LOCATION}.`;
84
53
  }
85
54
  if (payload.error.code === "insufficient_credits") {
86
- return "Insufficient Cite42 balance. Add funds at https://www.cite42.dev/app/billing.";
55
+ return `Insufficient Cite42 balance. Add funds in ${FUNDS_LOCATION}.`;
87
56
  }
88
57
  }
89
58
  } catch {
@@ -175,18 +144,18 @@ var modelEnum = z.enum(["chatgpt", "claude", "perplexity", "gemini", "google_ai_
175
144
  var QUERY_MAX_CHARS = 2e3;
176
145
  var SHORT_TEXT_MAX_CHARS = 200;
177
146
  var TIMEFRAME_MAX_CHARS = 80;
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.';
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.";
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. Recommendations take the requestId of a visibility result and the page URL, or a page URL alone for a free technical check; they run only when the user requests advice and knows any 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.";
182
- var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
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.";
184
- var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the tools that run successfully and the selected model rates.";
147
+ var MODEL_SELECTION_DESCRIPTION = "AI surfaces to query, each billed separately; pass a subset when the user names surfaces or wants a lower cost. Omit for the defaults (chatgpt, perplexity, gemini); fullSweep queries all five. google_ai_overview accepts queries up to 700 characters, the others 2000.";
148
+ var FULL_SWEEP_DESCRIPTION = "Query all five AI surfaces (adds claude and google_ai_overview) for full cross-surface coverage, at a higher cost. Ignored when models is given.";
149
+ var PAID_TOOL_BILLING = "Billed in USD (result field cite42Billing) to the user's prepaid Cite42 balance; exact prices from cite42_pricing (free). For plans over 20 calls or $5, estimate the cost and confirm with the user first.";
150
+ var RECOMMENDATIONS_INPUT_NOTE = "Its requestId can be passed to cite42_recommendations when the user asks for improvement advice.";
151
+ var MODEL_PRICING = "Priced per AI surface queried.";
152
+ var KEYWORDS_PRICING = "Billed by outcome: cheapest when the seed has no search volume, more when it has metrics, most with ideas:true.";
153
+ var WORKFLOW_PRICING = "Billed for the steps that succeed, at per-surface rates.";
185
154
  function workflowDescription(description) {
186
- return `${description} ${MCP_USAGE_GUIDANCE} ${MODEL_PRICING} ${DATA_PRICING} ${WORKFLOW_PRICING}`;
155
+ return `${description} ${WORKFLOW_PRICING} ${PAID_TOOL_BILLING}`;
187
156
  }
188
157
  function readOnly(title, openWorldHint = true) {
189
- return { annotations: { title, readOnlyHint: true, openWorldHint } };
158
+ return { annotations: { title, readOnlyHint: true, destructiveHint: false, openWorldHint } };
190
159
  }
191
160
  function mutation(title, options = {}) {
192
161
  return {
@@ -208,7 +177,7 @@ function shortText(description) {
208
177
  var aivRecommendations = createTool({
209
178
  id: "cite42_recommendations",
210
179
  mcp: readOnly("Recommend AI visibility next steps"),
211
- description: "Page-level AI visibility recommendations. With a saved result (requestId, trackerRunId or evidence) it reports on one measured prompt, charged only when the report includes page-comparison advice backed by quotes from inspected pages. Without one: url alone runs a free technical check of the page (robots.txt crawler access, indexing, rendering) and returns suggested buyer questions; url with query first runs a citations check, priced like cite42_citations, then the report. Prices are in cite42_pricing. Fetches the page, its robots.txt and up to two pages the AI surfaces cited, as returned HTML without JavaScript. No site-wide crawl, keyword advice or tracker activation. " + MCP_USAGE_GUIDANCE,
180
+ description: "Page-level AI visibility recommendations, for when the user asks how to improve a page. With a saved result (requestId, trackerRunId or evidence) it reports on one measured prompt, charged only when the report includes page-comparison advice backed by quotes from inspected pages. Without one: url alone runs a free technical check of the page (robots.txt crawler access, indexing, rendering) and returns suggested buyer questions; url with query first runs a citations check, priced like cite42_citations, then the report. Prices are in cite42_pricing. Fetches the page, its robots.txt and up to two pages the AI surfaces cited, as returned HTML without JavaScript. No site-wide crawl, keyword advice or tracker activation. " + PAID_TOOL_BILLING,
212
181
  inputSchema: z.object({
213
182
  requestId: z.string().trim().min(1).max(128).optional().describe("A requestId from your own successful Cite42 call within the last 30 days."),
214
183
  trackerRunId: z.string().trim().min(1).max(128).optional().describe("A specific finished run of your own tracker within the last 30 days."),
@@ -248,60 +217,52 @@ function optionalShortText(description) {
248
217
  var modelsInput = z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
249
218
  var fullSweepInput = z.boolean().optional().describe(FULL_SWEEP_DESCRIPTION);
250
219
  var competitorList = z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
220
+ async function fetchBalance() {
221
+ const usage = await aivGet("/usage");
222
+ const micro = usage.balance?.microCredits;
223
+ if (typeof micro !== "number" || !Number.isFinite(micro)) {
224
+ throw new Error("Cite42 usage response did not include a balance.");
225
+ }
226
+ return toCite42Balance(micro);
227
+ }
251
228
  var aivBalance = createTool({
252
229
  id: "cite42_balance",
253
230
  mcp: readOnly("Check Cite42 balance", false),
254
- description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
231
+ description: "Check the remaining Cite42 balance in USD for the connected account. Free.",
255
232
  inputSchema: z.object({}),
256
- execute: async () => {
257
- const usage = await aivGet("/usage");
258
- const microCredits = usage.balance?.microCredits;
259
- if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
260
- throw new Error("Cite42 usage response did not include a balance.");
261
- }
262
- return toCite42Balance(microCredits);
263
- }
233
+ execute: fetchBalance
264
234
  });
265
235
  var aivCredits = createTool({
266
236
  id: "cite42_credits",
267
237
  mcp: readOnly("Check Cite42 balance (legacy alias)", false),
268
- description: "Check the remaining Cite42 balance for the connected account. Free. " + MCP_USAGE_GUIDANCE,
238
+ description: "Alias of cite42_balance for older integrations. Free.",
269
239
  inputSchema: z.object({}),
270
- execute: async () => {
271
- const usage = await aivGet("/usage");
272
- const microCredits = usage.balance?.microCredits;
273
- if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
274
- throw new Error("Cite42 usage response did not include a balance.");
275
- }
276
- return toCite42CreditsBalance(microCredits);
277
- }
240
+ execute: fetchBalance
278
241
  });
279
242
  var aivPricing = createTool({
280
243
  id: "cite42_pricing",
281
244
  mcp: readOnly("Get Cite42 price table", false),
282
- 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,
283
- inputSchema: z.object({
284
- currency: z.enum(["USD", "credits"]).optional().describe("Use USD for dollar prices. Omit only for legacy compatibility.")
285
- }),
286
- execute: async (input) => input.currency === "USD" ? aivGet("/pricing?currency=USD") : { ...await aivGet("/pricing"), usd: await aivGet("/pricing?currency=USD") }
245
+ description: "Get the exact Cite42 price table in USD: per-surface rates, flat-rate tools, workflow maximums, and which tools are free. Use it for cost questions and to estimate a plan before running it. Free.",
246
+ // No currency switch: prices are USD only. An older client's `currency` argument is ignored.
247
+ inputSchema: z.object({}),
248
+ execute: async () => aivGet("/pricing?currency=USD")
287
249
  });
288
250
  var aivUsage = createTool({
289
251
  id: "cite42_usage",
290
252
  mcp: readOnly("Get recent Cite42 usage", false),
291
- 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,
253
+ description: "Get recent Cite42 usage for the connected account: the last calls with their cost and status, plus the remaining balance in USD. Free.",
292
254
  inputSchema: z.object({}),
293
255
  execute: async () => {
294
256
  const usage = await aivGet("/usage");
295
- const microCredits = usage.balance?.microCredits;
296
- if (typeof microCredits !== "number" || !Number.isFinite(microCredits)) {
257
+ const micro = usage.balance?.microCredits;
258
+ if (typeof micro !== "number" || !Number.isFinite(micro)) {
297
259
  throw new Error("Cite42 usage response did not include a balance.");
298
260
  }
299
261
  return {
300
- ...toCite42CreditsBalance(microCredits),
262
+ ...toCite42Balance(micro),
301
263
  recentCalls: (usage.recent ?? []).slice(0, 20).map((row) => ({
302
264
  endpoint: row.endpoint,
303
265
  status: row.status,
304
- credits: typeof row.costMicro === "number" ? toCredits(row.costMicro) : void 0,
305
266
  costUsd: typeof row.costMicro === "number" ? toUsd(row.costMicro) : void 0,
306
267
  at: row.createdAt
307
268
  }))
@@ -311,7 +272,7 @@ var aivUsage = createTool({
311
272
  var aivSearch = createTool({
312
273
  id: "cite42_search",
313
274
  mcp: readOnly("Search AI models"),
314
- 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}`,
275
+ 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} ${PAID_TOOL_BILLING}`,
315
276
  inputSchema: z.object({
316
277
  query: queryText("The concise AI-search/market-research query to run against the selected models."),
317
278
  models: modelsInput,
@@ -322,7 +283,7 @@ var aivSearch = createTool({
322
283
  var aivCitations = createTool({
323
284
  id: "cite42_citations",
324
285
  mcp: readOnly("Find AI citations"),
325
- description: `Find which sources a model's web search surfaces for a query. Aggregates cited URLs across models with per-model counts; optionally checks whether a specific URL is cited. Measures the retrieval corpus behind a prompt \u2014 which domains own your category in AI search, and where your content is missing \u2014 not the citation links a consumer chat UI renders for a logged-in user. ${MODEL_PRICING} ${MCP_USAGE_GUIDANCE}`,
286
+ description: `Find which sources a model's web search surfaces for a query. Aggregates cited URLs across models with per-model counts; optionally checks whether a specific URL is cited. Measures the retrieval corpus behind a prompt \u2014 which domains own your category in AI search, and where your content is missing \u2014 not the citation links a consumer chat UI renders for a logged-in user. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
326
287
  inputSchema: z.object({
327
288
  query: queryText("The concise query to look up cited sources for."),
328
289
  url: z.string().trim().max(500).optional().describe("Optional URL to check whether it was cited."),
@@ -334,7 +295,7 @@ var aivCitations = createTool({
334
295
  var aivRankings = createTool({
335
296
  id: "cite42_rankings",
336
297
  mcp: readOnly("Measure AI brand rankings"),
337
- 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}`,
298
+ 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. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
338
299
  inputSchema: z.object({
339
300
  query: queryText("The concise query to measure brand/competitor visibility on."),
340
301
  competitors: competitorList.describe("Brand or domain names to track within the AI-generated answers."),
@@ -346,7 +307,7 @@ var aivRankings = createTool({
346
307
  var aivCompare = createTool({
347
308
  id: "cite42_compare",
348
309
  mcp: readOnly("Compare brand against competitors"),
349
- 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}`,
310
+ 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. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
350
311
  inputSchema: z.object({
351
312
  query: queryText("The concise query to compare the brand and competitors on."),
352
313
  brand: shortText("Your brand, ranked alongside the competitors."),
@@ -359,7 +320,7 @@ var aivCompare = createTool({
359
320
  var aivSentiment = createTool({
360
321
  id: "cite42_sentiment",
361
322
  mcp: readOnly("Score brand sentiment in AI answers"),
362
- 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}`,
323
+ 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. ${RECOMMENDATIONS_INPUT_NOTE} ${MODEL_PRICING} ${PAID_TOOL_BILLING}`,
363
324
  inputSchema: z.object({
364
325
  query: queryText("The concise query whose answers are scored for brand sentiment."),
365
326
  brand: shortText("The brand to score sentiment for in each model answer."),
@@ -371,7 +332,7 @@ var aivSentiment = createTool({
371
332
  var aivKeywords = createTool({
372
333
  id: "cite42_keywords",
373
334
  mcp: readOnly("Get SEO keyword data"),
374
- 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. Not a multi-model AI-search query. The reply echoes requestedKeyword and carries status "exact" or "no_data"; when the seed has no search volume, any related term that does appears under alternatives \u2014 never treat an alternative's metrics as the requested keyword's. ${KEYWORDS_PRICING} ${MCP_USAGE_GUIDANCE}`,
335
+ 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. Not a multi-model AI-search query. The reply echoes requestedKeyword and carries status "exact" or "no_data"; when the seed has no search volume, any related term that does appears under alternatives \u2014 never treat an alternative's metrics as the requested keyword's. ${KEYWORDS_PRICING} ${PAID_TOOL_BILLING}`,
375
336
  inputSchema: z.object({
376
337
  seed: queryText("Seed keyword or concise topic to fetch search volume, CPC, and competition for."),
377
338
  country: z.string().trim().max(8).optional().describe('ISO-2 country code for localized metrics. Defaults to "us".'),
@@ -387,7 +348,7 @@ var aivKeywords = createTool({
387
348
  var aivKeywordsBatch = createTool({
388
349
  id: "cite42_keywords_batch",
389
350
  mcp: readOnly("Get batch SEO keyword data"),
390
- 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}`,
351
+ description: `Check search volume, CPC, and competition for two to 50 distinct seed keywords in one request. No related ideas. Billed by outcome. ${PAID_TOOL_BILLING}`,
391
352
  inputSchema: z.object({
392
353
  seeds: z.array(z.string().trim().min(1).max(80).refine(
393
354
  (seed) => seed.split(/\s+/).length <= 10,
@@ -404,7 +365,7 @@ var aivKeywordsBatch = createTool({
404
365
  var aivTrends = createTool({
405
366
  id: "cite42_trends",
406
367
  mcp: readOnly("Get Google Trends data"),
407
- 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}`,
368
+ 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. ${PAID_TOOL_BILLING}`,
408
369
  inputSchema: z.object({
409
370
  term: queryText("Search term or concise topic to pull Google Trends interest-over-time for."),
410
371
  timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trends window, e.g. "past_12_months", "past_30_days", "past_5_years".'),
@@ -415,7 +376,7 @@ var aivTrends = createTool({
415
376
  var aivRedditTrends = createTool({
416
377
  id: "cite42_reddit_trends",
417
378
  mcp: readOnly("Find Reddit discussions"),
418
- 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}`,
379
+ description: "Discover relevant Reddit discussions with Google Search and read up to three thread pages through Gemini URL context. Returns Gemini-read titles, authors, post excerpts, relative ages, available scores/comment counts and sampled human comments, plus cited model summaries. Check coverage and warnings: pages may be cached, extraction is not independently verified, and exact dates, growth and quote-based insights are unavailable. risingThreads is a deprecated alias of relevantThreads. " + PAID_TOOL_BILLING,
419
380
  inputSchema: z.object({
420
381
  topic: queryText("Topic, category, product, or audience to research on Reddit."),
421
382
  subreddits: z.array(shortText("Subreddit name without r/.")).max(20).optional().describe("Optional subreddit names, without r/."),
@@ -426,7 +387,7 @@ var aivRedditTrends = createTool({
426
387
  var aivYoutubeTrends = createTool({
427
388
  id: "cite42_youtube_trends",
428
389
  mcp: readOnly("Get YouTube trend data"),
429
- 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}`,
390
+ description: `YouTube trend data for rising videos, creator angles, title patterns, channel examples, and video-market opportunities around a topic. ${PAID_TOOL_BILLING}`,
430
391
  inputSchema: z.object({
431
392
  topic: queryText("Topic, keyword, niche, or audience to research on YouTube."),
432
393
  region: z.string().trim().max(8).optional().describe('ISO-2 country/region code. Defaults to "US".'),
@@ -447,7 +408,9 @@ var aivFindContentOpportunities = createTool({
447
408
  brand: optionalShortText("Optional brand/domain to account for."),
448
409
  competitors: competitorList.optional().describe("Competitor brands/domains."),
449
410
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
450
- channels: z.array(channelEnum).optional().describe("Optional data sources to include.")
411
+ channels: z.array(channelEnum).optional().describe("Optional data sources to include."),
412
+ models: modelsInput,
413
+ fullSweep: fullSweepInput
451
414
  }),
452
415
  execute: async (input, context) => aivPost("/workflows/content-opportunities", input, context?.abortSignal ?? context?.mcp?.extra.signal)
453
416
  });
@@ -460,7 +423,9 @@ var aivAnalyzeTopicDemand = createTool({
460
423
  inputSchema: z.object({
461
424
  topic: queryText("Topic, keyword, category, or audience demand to analyze."),
462
425
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
463
- timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
426
+ timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".'),
427
+ models: modelsInput,
428
+ fullSweep: fullSweepInput
464
429
  }),
465
430
  execute: async (input, context) => aivPost("/workflows/topic-demand", input, context?.abortSignal ?? context?.mcp?.extra.signal)
466
431
  });
@@ -468,7 +433,7 @@ var aivMapAiPrompts = createTool({
468
433
  id: "cite42_map_ai_prompts",
469
434
  mcp: readOnly("Map buyer prompts for AI search"),
470
435
  description: workflowDescription(
471
- "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."
436
+ "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; prompts written from it can be tracked with cite42_tracker_create."
472
437
  ),
473
438
  inputSchema: z.object({
474
439
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
@@ -476,7 +441,9 @@ var aivMapAiPrompts = createTool({
476
441
  brand: optionalShortText("Optional brand/domain to include in prompt tracking recommendations."),
477
442
  competitors: competitorList.optional().describe("Competitor brands/domains."),
478
443
  country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
479
- timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".')
444
+ timeframe: z.string().trim().max(TIMEFRAME_MAX_CHARS).optional().describe('Trend window. Defaults to "past_12_months".'),
445
+ models: modelsInput,
446
+ fullSweep: fullSweepInput
480
447
  }),
481
448
  execute: async (input, context) => aivPost("/workflows/ai-prompt-map", input, context?.abortSignal ?? context?.mcp?.extra.signal)
482
449
  });
@@ -490,7 +457,9 @@ var aivAnalyzeCompetitorContent = createTool({
490
457
  competitors: competitorList.describe("Competitor brands, domains, or channels to analyze."),
491
458
  topic: z.string().trim().min(1).max(QUERY_MAX_CHARS).optional().describe("Optional topic/category to constrain the analysis."),
492
459
  brand: optionalShortText("Optional primary brand/domain to compare against."),
493
- country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
460
+ country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
461
+ models: modelsInput,
462
+ fullSweep: fullSweepInput
494
463
  }),
495
464
  execute: async (input, context) => aivPost("/workflows/competitor-content", input, context?.abortSignal ?? context?.mcp?.extra.signal)
496
465
  });
@@ -521,7 +490,9 @@ var aivGenerateContentBrief = createTool({
521
490
  audience: optionalShortText("Optional target audience or ICP."),
522
491
  brand: optionalShortText("Optional brand/domain the brief is for."),
523
492
  goal: optionalShortText("Optional content goal, e.g. rank, earn citations, convert."),
524
- country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".')
493
+ country: z.string().trim().max(8).optional().describe('ISO-2 country code. Defaults to "us".'),
494
+ models: modelsInput,
495
+ fullSweep: fullSweepInput
525
496
  }),
526
497
  execute: async (input, context) => aivPost("/workflows/content-brief", input, context?.abortSignal ?? context?.mcp?.extra.signal)
527
498
  });
@@ -577,11 +548,11 @@ var trackerDefinitionFields = {
577
548
  "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."
578
549
  )
579
550
  };
580
- 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.";
551
+ var TRACKER_BILLING_DESCRIPTION = "Managing trackers and reading reports is free; each run bills one call per prompt at the per-surface rates.";
581
552
  var aivTrackerCreate = createTool({
582
553
  id: "cite42_tracker_create",
583
554
  mcp: mutation("Create Cite42 tracker draft"),
584
- 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,
555
+ 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. Prompt lists can come from cite42_map_ai_prompts research. Creation does not start recurring calls. " + TRACKER_BILLING_DESCRIPTION,
585
556
  inputSchema: z.object(trackerDefinitionFields),
586
557
  execute: async (input) => aivPost("/trackers", input)
587
558
  });
@@ -598,7 +569,7 @@ var aivTrackerActivate = createTool({
598
569
  openWorldHint: true,
599
570
  idempotentHint: true
600
571
  }),
601
- description: `Activate or resume recurring weekly or monthly tool calls. maxCostMicroPerRun must equal the tracker estimate being accepted; activation authorizes future billed calls until the tracker is paused. The response carries a billing block with the balance, the cost per run, and runsDryAt \u2014 the date the balance stops covering the account's active trackers \u2014 so relay it. When tracker emails are enabled, activation sends a setup summary and every completed run sends current results and suggested next steps, even when nothing changed. Activation is refused when the balance cannot cover even the next run; tell the user to top up at cite42.dev/app/billing. ${TRACKER_BILLING_DESCRIPTION}`,
572
+ description: `Activate or resume recurring weekly or monthly tool calls. maxCostMicroPerRun must equal the tracker's latest estimatedCostMicroPerRun; activation authorizes future billed calls until paused. The response includes the balance, the cost per run, and runsDryAt, the date the balance stops covering active trackers. When tracker emails are enabled, activation sends a setup summary and every completed run sends current results and suggested next steps, even when nothing changed. Activation is refused when the balance cannot cover the next run. ${TRACKER_BILLING_DESCRIPTION}`,
602
573
  inputSchema: z.object({
603
574
  trackerId: trackerIdInput,
604
575
  maxCostMicroPerRun: z.number().int().nonnegative().describe("Exact estimatedCostMicroPerRun returned by the latest tracker record.")
@@ -702,7 +673,7 @@ if (!resolveApiKey()) {
702
673
  );
703
674
  process.exit(1);
704
675
  }
705
- buildCite42Server("0.7.0").startStdio().catch((error) => {
676
+ buildCite42Server("0.8.0").startStdio().catch((error) => {
706
677
  console.error("[@cite42/mcp] Fatal error:", error);
707
678
  process.exit(1);
708
679
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cite42/mcp",
3
3
  "mcpName": "dev.cite42/mcp",
4
- "version": "0.7.0",
4
+ "version": "0.8.0",
5
5
  "description": "Cite42 MCP server: AI visibility and SEO data for AI agents. See how the AI models behind ChatGPT, Claude, Perplexity and Gemini, plus Google AI Overviews, mention your brand and which sources they cite, with SEO keyword data, Google, Reddit and YouTube trends, and 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",