@cite42/mcp 0.1.16 → 0.3.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
@@ -6,6 +6,7 @@ Once connected, Claude Code, Claude Desktop, Codex CLI, or Cursor can:
6
6
 
7
7
  - **Query AI models** (ChatGPT, Claude, Perplexity, Gemini, Google AI Overviews) and see each model's answer and the sources it cites
8
8
  - **Track brand visibility** in AI answers: rankings, head-to-head comparisons, and sentiment
9
+ - **Schedule daily or weekly trackers**: one measurement — rankings, comparisons, citations, or sentiment — over a list of prompts, with email when something changes, then ask your agent for stored run history
9
10
  - **Pull SEO data**: keyword search volume, CPC, competition, and Google Trends
10
11
  - **Mine Reddit and YouTube** for audience questions, pain points, and rising content
11
12
  - **Run full research workflows**: content gaps, topic demand, AI prompt maps, competitor analysis, and content briefs in a single call
@@ -91,7 +92,7 @@ You do not need to keep `npx -y @cite42/mcp` running in a separate terminal. Loc
91
92
  - **Claude Code:** run `claude mcp list` and check that `cite42` shows as ✔ connected. Or type `/mcp` inside a session.
92
93
  - **Claude Desktop:** open **Settings → Connectors** and confirm Cite42 is connected.
93
94
  - **Codex CLI:** run `codex mcp list`.
94
- - **Cursor:** look for `cite42` in the MCP/tools indicator. 15 tools should be listed.
95
+ - **Cursor:** look for `cite42` in the MCP/tools indicator. 26 tools should be listed.
95
96
 
96
97
  If it doesn't appear, see [Troubleshooting](#troubleshooting).
97
98
 
@@ -104,6 +105,7 @@ After connecting or restarting, just ask in plain language. Your agent picks the
104
105
  - *"Get search volume, CPC, and related keywords for 'ai seo tools'."*
105
106
  - *"What are people on Reddit complaining about regarding email marketing tools?"*
106
107
  - *"Run a content brief for the topic 'ai website builder'."*
108
+ - *"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."*
107
109
 
108
110
  ### Example: what a session looks like
109
111
 
@@ -113,18 +115,26 @@ After connecting or restarting, just ask in plain language. Your agent picks the
113
115
 
114
116
  **Your agent calls** `cite42_citations` **and answers something like:**
115
117
 
116
- > Across ChatGPT, Claude, Perplexity, and Gemini, the most-cited sources are:
118
+ > Across ChatGPT, Perplexity, and Gemini, the most-cited sources are:
117
119
  >
118
- > 1. **freshbooks.com** (cited by 4/5 models)
119
- > 2. **wave.com** (cited by 3/5 models)
120
- > 3. **zoho.com/invoice** (cited by 3/5 models)
121
- > 4. **bonsai.com** (cited by 2/5 models)
120
+ > 1. **freshbooks.com** (cited by 3/3 models)
121
+ > 2. **wave.com** (cited by 2/3 models)
122
+ > 3. **zoho.com/invoice** (cited by 2/3 models)
123
+ > 4. **bonsai.com** (cited by 1/3 models)
122
124
  >
123
125
  > Your domain wasn't cited by any model. FreshBooks is cited mainly via comparison/"best of" listicles. A citation-gap analysis (`cite42_find_ai_citation_gaps`) can show which queries you're missing from.
124
126
 
125
- The exact brands and sources change by query and model. A good result should show the tool used, model/source evidence, and a short summary you can act on.
127
+ That is the three default surfaces; ask for `fullSweep` to add Claude and Google AI Overviews. The exact brands and sources change by query and model. A good result should show the tool used, model/source evidence, and a short summary you can act on.
126
128
 
127
- ## Endpoint tools
129
+ ## Tools
130
+
131
+ Cite42 exposes 26 MCP tools: 3 free account tools, 9 primitive data tools, 6 research workflows, and 8 tracker tools.
132
+
133
+ ### Account & pricing (free)
134
+
135
+ - `cite42_credits`: check the connected account's remaining Cite42 credit balance.
136
+ - `cite42_pricing`: get current per-tool prices and estimate a planned set of calls.
137
+ - `cite42_usage`: read recent call status and cost together with the remaining balance.
128
138
 
129
139
  ### AI search visibility
130
140
 
@@ -145,11 +155,22 @@ The exact brands and sources change by query and model. A good result should sho
145
155
 
146
156
  - `cite42_find_content_opportunities`: find content gaps across AI search, keywords, trends, Reddit, YouTube, and citations.
147
157
  - `cite42_analyze_topic_demand`: combine keyword data, trends, AI answers, and audience conversations for a topic.
148
- - `cite42_map_ai_prompts`: discover and cluster buyer prompts worth tracking in AI search.
158
+ - `cite42_map_ai_prompts`: discover and cluster buyer prompts worth tracking in AI search. Its prompts are what `cite42_tracker_create` takes, so the two chain naturally.
149
159
  - `cite42_analyze_competitor_content`: analyze AI rankings, comparisons, citations, and topic coverage for competitors.
150
160
  - `cite42_find_ai_citation_gaps`: find queries where AI answers cite competitors but not your brand, domain, or URL.
151
161
  - `cite42_generate_content_brief`: collect AI answers, citations, keyword demand, and social signals into brief-ready data.
152
162
 
163
+ ### Trackers
164
+
165
+ - `cite42_tracker_create`: create a draft daily or weekly tracker — one measurement applied to a list of prompts. No prompt list yet? Run `cite42_map_ai_prompts` first. Creating a draft is free and does not schedule calls.
166
+ - `cite42_tracker_update`: replace the whole tracker definition — name, cadence, measurement, prompts and configuration. A tracker read returns the same shape, so read it, change what you need, and send it back. Updating is free.
167
+ - `cite42_tracker_activate`: activate or resume recurring runs. You must pass the exact `estimatedCostMicroPerRun` from the latest tracker record as `maxCostMicroPerRun`; activation authorizes billed scheduled runs until the tracker is paused. The response carries the balance, the cost per run and `runsDryAt` — the date the balance stops covering your active trackers — and activation is refused when the balance cannot cover even the next run.
168
+ - `cite42_tracker_pause`: stop future scheduled runs while preserving configuration and history. Pausing is free.
169
+ - `cite42_tracker_delete`: permanently delete a draft or paused tracker and its run history. Active trackers must be paused first, and deletion is rejected while a run is queued or running. Financial usage records remain available.
170
+ - `cite42_tracker_run`: queue one manual run of every saved prompt. Each prompt is billed as its own primitive call at the normal rate.
171
+ - `cite42_trackers`: list trackers, schedules, statuses, cost estimates, and last/next run times. Reading the list is free.
172
+ - `cite42_tracker_report`: read bounded stored run history with results, changes, status, sampled time, and costs (latest 8 by default, up to 20). Reading stored reports is free.
173
+
153
174
  ## Configuration
154
175
 
155
176
  - `CITE42_API_KEY`: required. Your API key from [www.cite42.dev/app/keys](https://www.cite42.dev/app/keys).
@@ -163,7 +184,7 @@ npx -y @cite42/mcp --api-key=cite42_live_your_key_here --api-base=https://www.ci
163
184
 
164
185
  ## Billing
165
186
 
166
- Tool calls consume Cite42 credits from your account. Calls fail with a clear error if the API key is missing or credits run out. See [www.cite42.dev/pricing](https://www.cite42.dev/pricing).
187
+ Primitive and workflow calls consume Cite42 credits from your account. Tracker management and stored-history reads are free; every scheduled or manual tracker run bills 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).
167
188
 
168
189
  ## Troubleshooting
169
190
 
package/dist/stdio.cjs CHANGED
@@ -87,9 +87,27 @@ function getBaseUrl() {
87
87
  return readFlag("api-base") ?? process.env.CITE42_API_BASE ?? DEFAULT_BASE_URL;
88
88
  }
89
89
  async function aivPost(path, body) {
90
+ return aivWrite("POST", path, body);
91
+ }
92
+ async function aivPatch(path, body) {
93
+ return aivWrite("PATCH", path, body);
94
+ }
95
+ async function aivDelete(path) {
96
+ const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
97
+ const res = await fetch(url, {
98
+ method: "DELETE",
99
+ headers: { Authorization: `Bearer ${getApiKey()}` }
100
+ });
101
+ if (!res.ok) {
102
+ const body = await res.text();
103
+ throw new Error(errorMessageForMcp(res.status, path, body));
104
+ }
105
+ return toCite42McpResult(await res.json());
106
+ }
107
+ async function aivWrite(method, path, body) {
90
108
  const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
91
109
  const res = await fetch(url, {
92
- method: "POST",
110
+ method,
93
111
  headers: {
94
112
  "Content-Type": "application/json",
95
113
  Authorization: `Bearer ${getApiKey()}`
@@ -125,9 +143,10 @@ var modelEnum = import_zod.z.enum(["chatgpt", "claude", "perplexity", "gemini",
125
143
  var QUERY_MAX_CHARS = 2e3;
126
144
  var SHORT_TEXT_MAX_CHARS = 200;
127
145
  var TIMEFRAME_MAX_CHARS = 80;
128
- var MODEL_SELECTION_DESCRIPTION = 'Optional model subset. Omit to query all five AI surfaces (chatgpt, claude, perplexity, gemini, google_ai_overview). Pass ["perplexity"] or ["perplexity","claude"] when the user asks for specific surfaces or wants to reduce cost.';
146
+ 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.';
147
+ 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.";
129
148
  var MCP_USAGE_GUIDANCE = "Covers AI-search, citation, SEO, keyword, trend, and market-research data. If a plan needs more than 20 endpoint calls or may use more than 500 Cite42 credits, estimate the credits and ask the user before continuing. When a result contains cite42Credits, that figure is Cite42 credits, not a model or host balance. Free (0 credits): cite42_credits for the remaining balance, cite42_pricing for exact per-tool prices and cost estimates, cite42_usage for recent calls.";
130
- var MODEL_PRICING = "Pricing is per selected AI surface; omitting models queries all five. Use the models field when the user names a surface or asks to reduce credit use, and call cite42_pricing for exact current rates.";
149
+ 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.";
131
150
  var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
132
151
  var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the primitive endpoints that run successfully and the selected model rates.";
133
152
  function workflowDescription(description) {
@@ -136,6 +155,17 @@ function workflowDescription(description) {
136
155
  function readOnly(title, openWorldHint = true) {
137
156
  return { annotations: { title, readOnlyHint: true, openWorldHint } };
138
157
  }
158
+ function mutation(title, options = {}) {
159
+ return {
160
+ annotations: {
161
+ title,
162
+ readOnlyHint: false,
163
+ destructiveHint: options.destructiveHint ?? false,
164
+ openWorldHint: options.openWorldHint ?? false,
165
+ ...options.idempotentHint === void 0 ? {} : { idempotentHint: options.idempotentHint }
166
+ }
167
+ };
168
+ }
139
169
  function queryText(description) {
140
170
  return import_zod.z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
141
171
  }
@@ -146,6 +176,7 @@ function optionalShortText(description) {
146
176
  return import_zod.z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).optional().describe(description);
147
177
  }
148
178
  var modelsInput = import_zod.z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
179
+ var fullSweepInput = import_zod.z.boolean().optional().describe(FULL_SWEEP_DESCRIPTION);
149
180
  var competitorList = import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
150
181
  var aivCredits = (0, import_tools.createTool)({
151
182
  id: "cite42_credits",
@@ -196,7 +227,8 @@ var aivSearch = (0, import_tools.createTool)({
196
227
  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}`,
197
228
  inputSchema: import_zod.z.object({
198
229
  query: queryText("The concise AI-search/market-research query to run against the selected models."),
199
- models: modelsInput
230
+ models: modelsInput,
231
+ fullSweep: fullSweepInput
200
232
  }),
201
233
  execute: async (input) => aivPost("/search", input)
202
234
  });
@@ -207,7 +239,8 @@ var aivCitations = (0, import_tools.createTool)({
207
239
  inputSchema: import_zod.z.object({
208
240
  query: queryText("The concise query to look up cited sources for."),
209
241
  url: import_zod.z.string().trim().max(500).optional().describe("Optional URL to check whether it was cited."),
210
- models: modelsInput
242
+ models: modelsInput,
243
+ fullSweep: fullSweepInput
211
244
  }),
212
245
  execute: async (input) => aivPost("/citations", input)
213
246
  });
@@ -218,7 +251,8 @@ var aivRankings = (0, import_tools.createTool)({
218
251
  inputSchema: import_zod.z.object({
219
252
  query: queryText("The concise query to measure brand/competitor visibility on."),
220
253
  competitors: competitorList.describe("Brand or domain names to track within the AI-generated answers."),
221
- models: modelsInput
254
+ models: modelsInput,
255
+ fullSweep: fullSweepInput
222
256
  }),
223
257
  execute: async (input) => aivPost("/rankings", input)
224
258
  });
@@ -230,7 +264,8 @@ var aivCompare = (0, import_tools.createTool)({
230
264
  query: queryText("The concise query to compare the brand and competitors on."),
231
265
  brand: shortText("Your brand, ranked alongside the competitors."),
232
266
  competitors: import_zod.z.array(shortText("Competitor brand/domain name.")).nonempty().max(10).describe("Competitor brand/domain names (up to 10) to rank against your brand."),
233
- models: modelsInput
267
+ models: modelsInput,
268
+ fullSweep: fullSweepInput
234
269
  }),
235
270
  execute: async (input) => aivPost("/compare", input)
236
271
  });
@@ -241,7 +276,8 @@ var aivSentiment = (0, import_tools.createTool)({
241
276
  inputSchema: import_zod.z.object({
242
277
  query: queryText("The concise query whose answers are scored for brand sentiment."),
243
278
  brand: shortText("The brand to score sentiment for in each model answer."),
244
- models: modelsInput
279
+ models: modelsInput,
280
+ fullSweep: fullSweepInput
245
281
  }),
246
282
  execute: async (input) => aivPost("/sentiment", input)
247
283
  });
@@ -325,7 +361,7 @@ var aivMapAiPrompts = (0, import_tools.createTool)({
325
361
  id: "cite42_map_ai_prompts",
326
362
  mcp: readOnly("Map buyer prompts for AI search"),
327
363
  description: workflowDescription(
328
- "Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data."
364
+ "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."
329
365
  ),
330
366
  inputSchema: import_zod.z.object({
331
367
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
@@ -362,7 +398,8 @@ var aivFindAiCitationGaps = (0, import_tools.createTool)({
362
398
  brand: shortText("Brand or domain that should earn citations."),
363
399
  url: import_zod.z.string().trim().max(500).optional().describe("Optional exact URL to check for citations."),
364
400
  competitors: competitorList.optional().describe("Competitor brands/domains."),
365
- models: modelsInput
401
+ models: modelsInput,
402
+ fullSweep: fullSweepInput
366
403
  }),
367
404
  execute: async (input) => aivPost("/workflows/ai-citation-gaps", input)
368
405
  });
@@ -381,6 +418,123 @@ var aivGenerateContentBrief = (0, import_tools.createTool)({
381
418
  }),
382
419
  execute: async (input) => aivPost("/workflows/content-brief", input)
383
420
  });
421
+ var trackerIdInput = import_zod.z.string().trim().min(1).max(128).describe("Cite42 tracker ID returned by a tracker management or report tool.");
422
+ var trackerCadenceInput = import_zod.z.object({
423
+ frequency: import_zod.z.enum(["daily", "weekly"]),
424
+ timezone: import_zod.z.string().trim().min(1).max(100).refine((timezone) => {
425
+ try {
426
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
427
+ return true;
428
+ } catch {
429
+ return false;
430
+ }
431
+ }, "Timezone must be a valid IANA timezone.").describe("IANA timezone, for example Europe/London or America/New_York."),
432
+ time: import_zod.z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe("Local run time in 24-hour HH:mm format."),
433
+ weekday: import_zod.z.number().int().min(0).max(6).optional().describe("Weekly day, where 0 is Sunday and 6 is Saturday.")
434
+ }).superRefine((cadence, context) => {
435
+ if (cadence.frequency === "weekly" && cadence.weekday === void 0) {
436
+ context.addIssue({
437
+ code: "custom",
438
+ path: ["weekday"],
439
+ message: "weekday is required for a weekly tracker"
440
+ });
441
+ }
442
+ });
443
+ var trackerPrimitiveInput = import_zod.z.enum(["rankings", "compare", "citations", "sentiment"]).describe(
444
+ "What to measure on every prompt: rankings (which brands get named), compare (the brand against named competitors), citations (which sources are cited), or sentiment (how one brand is described). One measurement per tracker."
445
+ );
446
+ var trackerPromptsInput = import_zod.z.array(queryText("A prompt to run on every scheduled run.")).min(1).max(25).describe(
447
+ "Prompts to track, up to 25. Each is billed as its own call on every run. Keep the wording stable so runs stay comparable, avoid dates, and do not repeat a prompt."
448
+ );
449
+ var trackerDefinitionFields = {
450
+ name: import_zod.z.string().trim().min(1).max(100).describe("Human-readable tracker name."),
451
+ cadence: trackerCadenceInput,
452
+ primitive: trackerPrimitiveInput,
453
+ prompts: trackerPromptsInput,
454
+ brand: import_zod.z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).nullish().describe("Required for compare and sentiment. Not used by rankings or citations."),
455
+ competitors: import_zod.z.array(shortText("Brand or domain name.")).max(20).nullish().describe(
456
+ "Required for rankings (the brands to track, including your own) and compare (the rivals to rank against, up to 10). Not used by citations or sentiment."
457
+ ),
458
+ url: import_zod.z.string().trim().min(1).max(500).nullish().describe("Citations only: a URL to report on as cited or not cited."),
459
+ models: import_zod.z.array(modelEnum).nonempty().nullish().describe(
460
+ "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."
461
+ )
462
+ };
463
+ 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.";
464
+ var aivTrackerCreate = (0, import_tools.createTool)({
465
+ id: "cite42_tracker_create",
466
+ mcp: mutation("Create Cite42 tracker draft"),
467
+ description: "Create a draft daily or weekly 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,
468
+ inputSchema: import_zod.z.object(trackerDefinitionFields),
469
+ execute: async (input) => aivPost("/trackers", input)
470
+ });
471
+ var aivTrackerUpdate = (0, import_tools.createTool)({
472
+ id: "cite42_tracker_update",
473
+ mcp: mutation("Update Cite42 tracker", { destructiveHint: true }),
474
+ description: `Replace a tracker definition: name, cadence, measurement, prompts and configuration. Send the complete definition, not a partial patch \u2014 a tracker read returns this exact shape, so read it, change what you need, and send it back. An edit returns the tracker to draft so its new recurring cost can be confirmed. ${TRACKER_BILLING_DESCRIPTION}`,
475
+ inputSchema: import_zod.z.object({ trackerId: trackerIdInput, ...trackerDefinitionFields }),
476
+ execute: async ({ trackerId, ...input }) => aivPatch(`/trackers/${encodeURIComponent(trackerId)}`, input)
477
+ });
478
+ var aivTrackerActivate = (0, import_tools.createTool)({
479
+ id: "cite42_tracker_activate",
480
+ mcp: mutation("Activate Cite42 tracker", {
481
+ openWorldHint: true,
482
+ idempotentHint: true
483
+ }),
484
+ description: `Activate or resume recurring daily or weekly primitive calls. maxCostMicroPerRun must equal the tracker estimate being accepted; activation authorizes future billed calls until the tracker is paused. 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. 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}`,
485
+ inputSchema: import_zod.z.object({
486
+ trackerId: trackerIdInput,
487
+ maxCostMicroPerRun: import_zod.z.number().int().nonnegative().describe("Exact estimatedCostMicroPerRun returned by the latest tracker record.")
488
+ }),
489
+ execute: async ({ trackerId, maxCostMicroPerRun }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/activate`, { maxCostMicroPerRun })
490
+ });
491
+ var aivTrackerPause = (0, import_tools.createTool)({
492
+ id: "cite42_tracker_pause",
493
+ mcp: mutation("Pause Cite42 tracker", { idempotentHint: true }),
494
+ description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free: costs 0 credits.",
495
+ inputSchema: import_zod.z.object({ trackerId: trackerIdInput }),
496
+ execute: async ({ trackerId }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/pause`, {})
497
+ });
498
+ var aivTrackerDelete = (0, import_tools.createTool)({
499
+ id: "cite42_tracker_delete",
500
+ mcp: mutation("Permanently delete Cite42 tracker", {
501
+ destructiveHint: true,
502
+ idempotentHint: true
503
+ }),
504
+ 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.",
505
+ inputSchema: import_zod.z.object({ trackerId: trackerIdInput }),
506
+ execute: async ({ trackerId }) => aivDelete(`/trackers/${encodeURIComponent(trackerId)}`)
507
+ });
508
+ var aivTrackerRun = (0, import_tools.createTool)({
509
+ id: "cite42_tracker_run",
510
+ mcp: mutation("Run Cite42 tracker now", {
511
+ openWorldHint: true,
512
+ idempotentHint: true
513
+ }),
514
+ description: `Queue one manual run using the tracker\u2019s saved primitive inputs. A repeated requestToken returns the same logical run. ${TRACKER_BILLING_DESCRIPTION}`,
515
+ inputSchema: import_zod.z.object({
516
+ trackerId: trackerIdInput,
517
+ requestToken: import_zod.z.string().trim().min(1).max(200).describe("Stable caller-generated token used to deduplicate a retried manual run.")
518
+ }),
519
+ execute: async ({ trackerId, requestToken }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/runs`, { requestToken })
520
+ });
521
+ var aivTrackers = (0, import_tools.createTool)({
522
+ id: "cite42_trackers",
523
+ mcp: readOnly("List Cite42 trackers", false),
524
+ description: "List the connected account\u2019s trackers with status, cadence, primitive configuration, cost estimate, accepted per-run maximum, and next/last run times. Free: costs 0 credits.",
525
+ inputSchema: import_zod.z.object({}),
526
+ execute: async () => aivGet("/trackers")
527
+ });
528
+ var aivTrackerReport = (0, import_tools.createTool)({
529
+ id: "cite42_tracker_report",
530
+ mcp: readOnly("Read Cite42 tracker report", false),
531
+ description: "Read bounded stored run history for one tracker, including per-primitive results, changes from the prior comparable observation, statuses, sampled times, and costs. Free: costs 0 credits.",
532
+ inputSchema: import_zod.z.object({
533
+ trackerId: trackerIdInput,
534
+ limit: import_zod.z.number().int().min(1).max(20).optional().default(8).describe("Most recent runs to return, capped for LLM context.")
535
+ }),
536
+ execute: async ({ trackerId, limit }) => aivGet(`/trackers/${encodeURIComponent(trackerId)}/report?limit=${limit}`)
537
+ });
384
538
 
385
539
  // src/server.ts
386
540
  var CITE42_TOOLS = {
@@ -401,7 +555,15 @@ var CITE42_TOOLS = {
401
555
  cite42_map_ai_prompts: aivMapAiPrompts,
402
556
  cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
403
557
  cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
404
- cite42_generate_content_brief: aivGenerateContentBrief
558
+ cite42_generate_content_brief: aivGenerateContentBrief,
559
+ cite42_tracker_create: aivTrackerCreate,
560
+ cite42_tracker_update: aivTrackerUpdate,
561
+ cite42_tracker_activate: aivTrackerActivate,
562
+ cite42_tracker_pause: aivTrackerPause,
563
+ cite42_tracker_delete: aivTrackerDelete,
564
+ cite42_tracker_run: aivTrackerRun,
565
+ cite42_trackers: aivTrackers,
566
+ cite42_tracker_report: aivTrackerReport
405
567
  };
406
568
  function buildCite42Server(version) {
407
569
  return new import_mcp.MCPServer({
@@ -420,7 +582,7 @@ if (!resolveApiKey()) {
420
582
  );
421
583
  process.exit(1);
422
584
  }
423
- buildCite42Server("0.1.16").startStdio().catch((error) => {
585
+ buildCite42Server("0.3.0").startStdio().catch((error) => {
424
586
  console.error("[@cite42/mcp] Fatal error:", error);
425
587
  process.exit(1);
426
588
  });
package/dist/stdio.mjs CHANGED
@@ -86,9 +86,27 @@ function getBaseUrl() {
86
86
  return readFlag("api-base") ?? process.env.CITE42_API_BASE ?? DEFAULT_BASE_URL;
87
87
  }
88
88
  async function aivPost(path, body) {
89
+ return aivWrite("POST", path, body);
90
+ }
91
+ async function aivPatch(path, body) {
92
+ return aivWrite("PATCH", path, body);
93
+ }
94
+ async function aivDelete(path) {
95
+ const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
96
+ const res = await fetch(url, {
97
+ method: "DELETE",
98
+ headers: { Authorization: `Bearer ${getApiKey()}` }
99
+ });
100
+ if (!res.ok) {
101
+ const body = await res.text();
102
+ throw new Error(errorMessageForMcp(res.status, path, body));
103
+ }
104
+ return toCite42McpResult(await res.json());
105
+ }
106
+ async function aivWrite(method, path, body) {
89
107
  const url = `${getBaseUrl()}${path.startsWith("/") ? path : `/${path}`}`;
90
108
  const res = await fetch(url, {
91
- method: "POST",
109
+ method,
92
110
  headers: {
93
111
  "Content-Type": "application/json",
94
112
  Authorization: `Bearer ${getApiKey()}`
@@ -124,9 +142,10 @@ var modelEnum = z.enum(["chatgpt", "claude", "perplexity", "gemini", "google_ai_
124
142
  var QUERY_MAX_CHARS = 2e3;
125
143
  var SHORT_TEXT_MAX_CHARS = 200;
126
144
  var TIMEFRAME_MAX_CHARS = 80;
127
- var MODEL_SELECTION_DESCRIPTION = 'Optional model subset. Omit to query all five AI surfaces (chatgpt, claude, perplexity, gemini, google_ai_overview). Pass ["perplexity"] or ["perplexity","claude"] when the user asks for specific surfaces or wants to reduce cost.';
145
+ 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.';
146
+ 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.";
128
147
  var MCP_USAGE_GUIDANCE = "Covers AI-search, citation, SEO, keyword, trend, and market-research data. If a plan needs more than 20 endpoint calls or may use more than 500 Cite42 credits, estimate the credits and ask the user before continuing. When a result contains cite42Credits, that figure is Cite42 credits, not a model or host balance. Free (0 credits): cite42_credits for the remaining balance, cite42_pricing for exact per-tool prices and cost estimates, cite42_usage for recent calls.";
129
- var MODEL_PRICING = "Pricing is per selected AI surface; omitting models queries all five. Use the models field when the user names a surface or asks to reduce credit use, and call cite42_pricing for exact current rates.";
148
+ 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.";
130
149
  var DATA_PRICING = "Data tools have flat per-call prices; call cite42_pricing for exact current rates.";
131
150
  var WORKFLOW_PRICING = "Pricing: workflow cost is calculated from the primitive endpoints that run successfully and the selected model rates.";
132
151
  function workflowDescription(description) {
@@ -135,6 +154,17 @@ function workflowDescription(description) {
135
154
  function readOnly(title, openWorldHint = true) {
136
155
  return { annotations: { title, readOnlyHint: true, openWorldHint } };
137
156
  }
157
+ function mutation(title, options = {}) {
158
+ return {
159
+ annotations: {
160
+ title,
161
+ readOnlyHint: false,
162
+ destructiveHint: options.destructiveHint ?? false,
163
+ openWorldHint: options.openWorldHint ?? false,
164
+ ...options.idempotentHint === void 0 ? {} : { idempotentHint: options.idempotentHint }
165
+ }
166
+ };
167
+ }
138
168
  function queryText(description) {
139
169
  return z.string().trim().min(1).max(QUERY_MAX_CHARS).describe(description);
140
170
  }
@@ -145,6 +175,7 @@ function optionalShortText(description) {
145
175
  return z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).optional().describe(description);
146
176
  }
147
177
  var modelsInput = z.array(modelEnum).nonempty().optional().describe(MODEL_SELECTION_DESCRIPTION);
178
+ var fullSweepInput = z.boolean().optional().describe(FULL_SWEEP_DESCRIPTION);
148
179
  var competitorList = z.array(shortText("Competitor brand/domain name.")).nonempty().max(20);
149
180
  var aivCredits = createTool({
150
181
  id: "cite42_credits",
@@ -195,7 +226,8 @@ var aivSearch = createTool({
195
226
  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}`,
196
227
  inputSchema: z.object({
197
228
  query: queryText("The concise AI-search/market-research query to run against the selected models."),
198
- models: modelsInput
229
+ models: modelsInput,
230
+ fullSweep: fullSweepInput
199
231
  }),
200
232
  execute: async (input) => aivPost("/search", input)
201
233
  });
@@ -206,7 +238,8 @@ var aivCitations = createTool({
206
238
  inputSchema: z.object({
207
239
  query: queryText("The concise query to look up cited sources for."),
208
240
  url: z.string().trim().max(500).optional().describe("Optional URL to check whether it was cited."),
209
- models: modelsInput
241
+ models: modelsInput,
242
+ fullSweep: fullSweepInput
210
243
  }),
211
244
  execute: async (input) => aivPost("/citations", input)
212
245
  });
@@ -217,7 +250,8 @@ var aivRankings = createTool({
217
250
  inputSchema: z.object({
218
251
  query: queryText("The concise query to measure brand/competitor visibility on."),
219
252
  competitors: competitorList.describe("Brand or domain names to track within the AI-generated answers."),
220
- models: modelsInput
253
+ models: modelsInput,
254
+ fullSweep: fullSweepInput
221
255
  }),
222
256
  execute: async (input) => aivPost("/rankings", input)
223
257
  });
@@ -229,7 +263,8 @@ var aivCompare = createTool({
229
263
  query: queryText("The concise query to compare the brand and competitors on."),
230
264
  brand: shortText("Your brand, ranked alongside the competitors."),
231
265
  competitors: z.array(shortText("Competitor brand/domain name.")).nonempty().max(10).describe("Competitor brand/domain names (up to 10) to rank against your brand."),
232
- models: modelsInput
266
+ models: modelsInput,
267
+ fullSweep: fullSweepInput
233
268
  }),
234
269
  execute: async (input) => aivPost("/compare", input)
235
270
  });
@@ -240,7 +275,8 @@ var aivSentiment = createTool({
240
275
  inputSchema: z.object({
241
276
  query: queryText("The concise query whose answers are scored for brand sentiment."),
242
277
  brand: shortText("The brand to score sentiment for in each model answer."),
243
- models: modelsInput
278
+ models: modelsInput,
279
+ fullSweep: fullSweepInput
244
280
  }),
245
281
  execute: async (input) => aivPost("/sentiment", input)
246
282
  });
@@ -324,7 +360,7 @@ var aivMapAiPrompts = createTool({
324
360
  id: "cite42_map_ai_prompts",
325
361
  mcp: readOnly("Map buyer prompts for AI search"),
326
362
  description: workflowDescription(
327
- "Run the AI-prompt-map workflow: discover and cluster buyer prompts worth tracking from AI search, SEO demand, trends, citations, Reddit, and YouTube data."
363
+ "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."
328
364
  ),
329
365
  inputSchema: z.object({
330
366
  topic: queryText("Market, product category, buyer problem, or audience to map prompts for."),
@@ -361,7 +397,8 @@ var aivFindAiCitationGaps = createTool({
361
397
  brand: shortText("Brand or domain that should earn citations."),
362
398
  url: z.string().trim().max(500).optional().describe("Optional exact URL to check for citations."),
363
399
  competitors: competitorList.optional().describe("Competitor brands/domains."),
364
- models: modelsInput
400
+ models: modelsInput,
401
+ fullSweep: fullSweepInput
365
402
  }),
366
403
  execute: async (input) => aivPost("/workflows/ai-citation-gaps", input)
367
404
  });
@@ -380,6 +417,123 @@ var aivGenerateContentBrief = createTool({
380
417
  }),
381
418
  execute: async (input) => aivPost("/workflows/content-brief", input)
382
419
  });
420
+ var trackerIdInput = z.string().trim().min(1).max(128).describe("Cite42 tracker ID returned by a tracker management or report tool.");
421
+ var trackerCadenceInput = z.object({
422
+ frequency: z.enum(["daily", "weekly"]),
423
+ timezone: z.string().trim().min(1).max(100).refine((timezone) => {
424
+ try {
425
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
426
+ return true;
427
+ } catch {
428
+ return false;
429
+ }
430
+ }, "Timezone must be a valid IANA timezone.").describe("IANA timezone, for example Europe/London or America/New_York."),
431
+ time: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/).describe("Local run time in 24-hour HH:mm format."),
432
+ weekday: z.number().int().min(0).max(6).optional().describe("Weekly day, where 0 is Sunday and 6 is Saturday.")
433
+ }).superRefine((cadence, context) => {
434
+ if (cadence.frequency === "weekly" && cadence.weekday === void 0) {
435
+ context.addIssue({
436
+ code: "custom",
437
+ path: ["weekday"],
438
+ message: "weekday is required for a weekly tracker"
439
+ });
440
+ }
441
+ });
442
+ var trackerPrimitiveInput = z.enum(["rankings", "compare", "citations", "sentiment"]).describe(
443
+ "What to measure on every prompt: rankings (which brands get named), compare (the brand against named competitors), citations (which sources are cited), or sentiment (how one brand is described). One measurement per tracker."
444
+ );
445
+ var trackerPromptsInput = z.array(queryText("A prompt to run on every scheduled run.")).min(1).max(25).describe(
446
+ "Prompts to track, up to 25. Each is billed as its own call on every run. Keep the wording stable so runs stay comparable, avoid dates, and do not repeat a prompt."
447
+ );
448
+ var trackerDefinitionFields = {
449
+ name: z.string().trim().min(1).max(100).describe("Human-readable tracker name."),
450
+ cadence: trackerCadenceInput,
451
+ primitive: trackerPrimitiveInput,
452
+ prompts: trackerPromptsInput,
453
+ brand: z.string().trim().min(1).max(SHORT_TEXT_MAX_CHARS).nullish().describe("Required for compare and sentiment. Not used by rankings or citations."),
454
+ competitors: z.array(shortText("Brand or domain name.")).max(20).nullish().describe(
455
+ "Required for rankings (the brands to track, including your own) and compare (the rivals to rank against, up to 10). Not used by citations or sentiment."
456
+ ),
457
+ url: z.string().trim().min(1).max(500).nullish().describe("Citations only: a URL to report on as cited or not cited."),
458
+ models: z.array(modelEnum).nonempty().nullish().describe(
459
+ "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."
460
+ )
461
+ };
462
+ 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.";
463
+ var aivTrackerCreate = createTool({
464
+ id: "cite42_tracker_create",
465
+ mcp: mutation("Create Cite42 tracker draft"),
466
+ description: "Create a draft daily or weekly 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,
467
+ inputSchema: z.object(trackerDefinitionFields),
468
+ execute: async (input) => aivPost("/trackers", input)
469
+ });
470
+ var aivTrackerUpdate = createTool({
471
+ id: "cite42_tracker_update",
472
+ mcp: mutation("Update Cite42 tracker", { destructiveHint: true }),
473
+ description: `Replace a tracker definition: name, cadence, measurement, prompts and configuration. Send the complete definition, not a partial patch \u2014 a tracker read returns this exact shape, so read it, change what you need, and send it back. An edit returns the tracker to draft so its new recurring cost can be confirmed. ${TRACKER_BILLING_DESCRIPTION}`,
474
+ inputSchema: z.object({ trackerId: trackerIdInput, ...trackerDefinitionFields }),
475
+ execute: async ({ trackerId, ...input }) => aivPatch(`/trackers/${encodeURIComponent(trackerId)}`, input)
476
+ });
477
+ var aivTrackerActivate = createTool({
478
+ id: "cite42_tracker_activate",
479
+ mcp: mutation("Activate Cite42 tracker", {
480
+ openWorldHint: true,
481
+ idempotentHint: true
482
+ }),
483
+ description: `Activate or resume recurring daily or weekly primitive calls. maxCostMicroPerRun must equal the tracker estimate being accepted; activation authorizes future billed calls until the tracker is paused. 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. 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}`,
484
+ inputSchema: z.object({
485
+ trackerId: trackerIdInput,
486
+ maxCostMicroPerRun: z.number().int().nonnegative().describe("Exact estimatedCostMicroPerRun returned by the latest tracker record.")
487
+ }),
488
+ execute: async ({ trackerId, maxCostMicroPerRun }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/activate`, { maxCostMicroPerRun })
489
+ });
490
+ var aivTrackerPause = createTool({
491
+ id: "cite42_tracker_pause",
492
+ mcp: mutation("Pause Cite42 tracker", { idempotentHint: true }),
493
+ description: "Pause future recurring calls for a tracker while preserving its configuration and stored run history. Free: costs 0 credits.",
494
+ inputSchema: z.object({ trackerId: trackerIdInput }),
495
+ execute: async ({ trackerId }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/pause`, {})
496
+ });
497
+ var aivTrackerDelete = createTool({
498
+ id: "cite42_tracker_delete",
499
+ mcp: mutation("Permanently delete Cite42 tracker", {
500
+ destructiveHint: true,
501
+ idempotentHint: true
502
+ }),
503
+ 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.",
504
+ inputSchema: z.object({ trackerId: trackerIdInput }),
505
+ execute: async ({ trackerId }) => aivDelete(`/trackers/${encodeURIComponent(trackerId)}`)
506
+ });
507
+ var aivTrackerRun = createTool({
508
+ id: "cite42_tracker_run",
509
+ mcp: mutation("Run Cite42 tracker now", {
510
+ openWorldHint: true,
511
+ idempotentHint: true
512
+ }),
513
+ description: `Queue one manual run using the tracker\u2019s saved primitive inputs. A repeated requestToken returns the same logical run. ${TRACKER_BILLING_DESCRIPTION}`,
514
+ inputSchema: z.object({
515
+ trackerId: trackerIdInput,
516
+ requestToken: z.string().trim().min(1).max(200).describe("Stable caller-generated token used to deduplicate a retried manual run.")
517
+ }),
518
+ execute: async ({ trackerId, requestToken }) => aivPost(`/trackers/${encodeURIComponent(trackerId)}/runs`, { requestToken })
519
+ });
520
+ var aivTrackers = createTool({
521
+ id: "cite42_trackers",
522
+ mcp: readOnly("List Cite42 trackers", false),
523
+ description: "List the connected account\u2019s trackers with status, cadence, primitive configuration, cost estimate, accepted per-run maximum, and next/last run times. Free: costs 0 credits.",
524
+ inputSchema: z.object({}),
525
+ execute: async () => aivGet("/trackers")
526
+ });
527
+ var aivTrackerReport = createTool({
528
+ id: "cite42_tracker_report",
529
+ mcp: readOnly("Read Cite42 tracker report", false),
530
+ description: "Read bounded stored run history for one tracker, including per-primitive results, changes from the prior comparable observation, statuses, sampled times, and costs. Free: costs 0 credits.",
531
+ inputSchema: z.object({
532
+ trackerId: trackerIdInput,
533
+ limit: z.number().int().min(1).max(20).optional().default(8).describe("Most recent runs to return, capped for LLM context.")
534
+ }),
535
+ execute: async ({ trackerId, limit }) => aivGet(`/trackers/${encodeURIComponent(trackerId)}/report?limit=${limit}`)
536
+ });
383
537
 
384
538
  // src/server.ts
385
539
  var CITE42_TOOLS = {
@@ -400,7 +554,15 @@ var CITE42_TOOLS = {
400
554
  cite42_map_ai_prompts: aivMapAiPrompts,
401
555
  cite42_analyze_competitor_content: aivAnalyzeCompetitorContent,
402
556
  cite42_find_ai_citation_gaps: aivFindAiCitationGaps,
403
- cite42_generate_content_brief: aivGenerateContentBrief
557
+ cite42_generate_content_brief: aivGenerateContentBrief,
558
+ cite42_tracker_create: aivTrackerCreate,
559
+ cite42_tracker_update: aivTrackerUpdate,
560
+ cite42_tracker_activate: aivTrackerActivate,
561
+ cite42_tracker_pause: aivTrackerPause,
562
+ cite42_tracker_delete: aivTrackerDelete,
563
+ cite42_tracker_run: aivTrackerRun,
564
+ cite42_trackers: aivTrackers,
565
+ cite42_tracker_report: aivTrackerReport
404
566
  };
405
567
  function buildCite42Server(version) {
406
568
  return new MCPServer({
@@ -419,7 +581,7 @@ if (!resolveApiKey()) {
419
581
  );
420
582
  process.exit(1);
421
583
  }
422
- buildCite42Server("0.1.16").startStdio().catch((error) => {
584
+ buildCite42Server("0.3.0").startStdio().catch((error) => {
423
585
  console.error("[@cite42/mcp] Fatal error:", error);
424
586
  process.exit(1);
425
587
  });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cite42/mcp",
3
3
  "mcpName": "dev.cite42/mcp",
4
- "version": "0.1.16",
5
- "description": "Cite42 MCP server: query ChatGPT, Claude, Perplexity, Gemini & Google AI Overviews for brand rankings, AI citations, SEO keyword data, and Reddit/YouTube trends. Works with Claude Code, Claude Desktop, Codex CLI, Cursor, and any MCP client.",
4
+ "version": "0.3.0",
5
+ "description": "Cite42 MCP server: query ChatGPT, Claude, Perplexity, Gemini & Google AI Overviews for brand rankings, AI citations, SEO keyword data, and Reddit/YouTube trends, and schedule daily or weekly 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",
8
8
  "bin": {
@@ -60,7 +60,10 @@
60
60
  "claude-code",
61
61
  "cursor",
62
62
  "reddit",
63
- "youtube"
63
+ "youtube",
64
+ "ai-visibility-tracking",
65
+ "rank-tracking",
66
+ "scheduled-monitoring"
64
67
  ],
65
68
  "engines": {
66
69
  "node": ">=18"