@datadive-tools/mcp 0.3.0 → 0.5.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
@@ -44,7 +44,7 @@ Add the `datadive` entry under `mcpServers`:
44
44
  "command": "npx",
45
45
  "args": ["-y", "@datadive-tools/mcp"],
46
46
  "env": {
47
- "DATADIVE_API_KEY": "ddk_live_..."
47
+ "DATADIVE_API_KEY": "YOUR_API_KEY"
48
48
  }
49
49
  }
50
50
  }
@@ -58,7 +58,7 @@ Restart Claude Desktop. You should see `datadive` in the tools menu.
58
58
  ```sh
59
59
  claude mcp add datadive -- npx -y @datadive-tools/mcp
60
60
  # Then add the env var via:
61
- # claude mcp add datadive --env DATADIVE_API_KEY=ddk_live_... -- npx -y @datadive-tools/mcp
61
+ # claude mcp add datadive --env DATADIVE_API_KEY=YOUR_API_KEY -- npx -y @datadive-tools/mcp
62
62
  ```
63
63
 
64
64
  Or edit `.mcp.json` in your project / `~/.claude/mcp.json` globally with the
@@ -73,7 +73,7 @@ same JSON shape as above.
73
73
  "datadive": {
74
74
  "command": "npx",
75
75
  "args": ["-y", "@datadive-tools/mcp"],
76
- "env": { "DATADIVE_API_KEY": "ddk_live_..." }
76
+ "env": { "DATADIVE_API_KEY": "YOUR_API_KEY" }
77
77
  }
78
78
  }
79
79
  ```
@@ -85,17 +85,22 @@ Ask Claude: **"List my DataDive niches."**
85
85
  You should see a tool call to `list_niches` and a JSON response with your
86
86
  niches, plus pagination metadata. If you don't, see Troubleshooting below.
87
87
 
88
- ## Available tools (v0.1)
88
+ ## Available tools
89
89
 
90
90
  | Tool | Description |
91
91
  |---|---|
92
- | `list_niches` | Paginated list of your niches. Discovery step — returns `nicheId`s for the next four tools. |
92
+ | `list_niches` | Paginated list of your niches. Discovery step — returns `nicheId`s for the niche-scoped tools below. |
93
93
  | `get_niche_keywords` | Master keyword list for a niche: search volume, relevancy, competitor ASIN ranks. |
94
+ | `get_niche_roots` | Keyword lexical roots for a niche — high-impact words with frequency and broad search volume. |
94
95
  | `get_niche_competitors` | Competitor ASINs and niche statistics (sales, revenue, ratings, opportunity score). |
95
96
  | `get_ranking_juice` | DataDive proprietary ranking-juice metric per competitor (current vs optimized listing). |
96
97
  | `list_rank_radars` | Paginated list of rank radars. Filter by `nicheId` or `status`. |
97
98
  | `get_rank_radar_data` | Historical keyword rankings for a rank radar within a `startDate`/`endDate` range. |
98
99
  | `get_asin_inventory_distribution` | Per-fulfillment-center sellable inventory for an ASIN. Requires `sellerId` from your Connections page. |
100
+ | `list_indexing_issue_alerts` | Paginated list of indexing-issue alerts — ASINs no longer indexed for their tracked keywords. Filter by `sellerId`, `marketplace`, `status`, or `updatedSince`. |
101
+ | `list_blind_spend_alerts` | Paginated list of blind-spend alerts — ad spend on search terms with little or no sales, with per-term spend/clicks/CVR. Same filters as above. |
102
+ | `get_quota` | Current quota usage and capacity per billable feature, plus the next refresh date. No arguments. |
103
+ | `list_usage` | Paginated billable usage logs (token-consumption events). Filter by `type`, `search` (user), and `startDate`/`endDate`. |
99
104
 
100
105
  Read-only. All data is scoped to the organization that owns the API key.
101
106
 
@@ -152,16 +157,10 @@ npm run typecheck # tsc --noEmit
152
157
  npm run lint # eslint
153
158
  ```
154
159
 
155
- Smoke test against staging:
156
-
157
- ```sh
158
- DATADIVE_SMOKE_API_KEY=ddk_test_... DATADIVE_API_BASE_URL=https://api-staging.datadive.tools npm run test:smoke
159
- ```
160
-
161
160
  Test the binary end-to-end with the MCP Inspector:
162
161
 
163
162
  ```sh
164
- DATADIVE_API_KEY=ddk_live_... npx @modelcontextprotocol/inspector dist/index.js
163
+ DATADIVE_API_KEY=YOUR_API_KEY npx @modelcontextprotocol/inspector dist/index.js
165
164
  ```
166
165
 
167
166
  ## Releasing
package/dist/index.js CHANGED
@@ -100,7 +100,7 @@ function extractMessage(body) {
100
100
  }
101
101
 
102
102
  // package.json
103
- var version = "0.3.0";
103
+ var version = "0.5.0";
104
104
 
105
105
  // src/http/client.ts
106
106
  var PKG_VERSION = version;
@@ -224,16 +224,34 @@ var getNicheKeywordsTool = {
224
224
  }
225
225
  };
226
226
 
227
- // src/tools/get-niche-competitors.ts
227
+ // src/tools/get-niche-roots.ts
228
228
  import { z as z3 } from "zod";
229
229
  var inputSchema3 = {
230
230
  nicheId: z3.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
231
231
  };
232
+ var getNicheRootsTool = {
233
+ name: "get_niche_roots",
234
+ title: "Get Keyword Roots for a Niche",
235
+ description: "Use this when the user wants to find the highest-impact words across a niche's keywords, asks about 'keyword roots', or wants to prioritize terms for a listing. Retrieves the keyword lexical roots for the Niche \u2014 individual words or word-combinations extracted from the Master Keyword List (e.g. 'bluetooth headphones' yields roots 'bluetooth', 'headphones', 'bluetooth headphones'). Returns `roots` and `normalizedRoots` tables, each item with root, frequency (how many keywords contain it), broadSearchVolume (summed search volume of those keywords), and broadSearchVolumeRatio (0-1, relative to the top root), plus the per-keyword breakdowns and latestResearchDate.",
236
+ inputSchema: inputSchema3,
237
+ handler: async (args, ctx) => {
238
+ return await httpGet(
239
+ { config: ctx.config, toolName: "get_niche_roots" },
240
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/roots`
241
+ );
242
+ }
243
+ };
244
+
245
+ // src/tools/get-niche-competitors.ts
246
+ import { z as z4 } from "zod";
247
+ var inputSchema4 = {
248
+ nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
249
+ };
232
250
  var getNicheCompetitorsTool = {
233
251
  name: "get_niche_competitors",
234
252
  title: "Get Competitors and Niche Statistics",
235
253
  description: "Use this for competitor ASINs, sales/revenue benchmarks, or niche opportunity scoring. Retrieves the list of Competitors within the specified Niche along with Niche statistics. For the Niche: keyword statistics, opportunity evaluation, benchmark median values, and overall competitor strength assessment. For each Competitor: ASIN, sales, revenue, ratings, reviews, price, image URL, and ranking data.",
236
- inputSchema: inputSchema3,
254
+ inputSchema: inputSchema4,
237
255
  handler: async (args, ctx) => {
238
256
  return await httpGet(
239
257
  { config: ctx.config, toolName: "get_niche_competitors" },
@@ -243,15 +261,15 @@ var getNicheCompetitorsTool = {
243
261
  };
244
262
 
245
263
  // src/tools/get-ranking-juice.ts
246
- import { z as z4 } from "zod";
247
- var inputSchema4 = {
248
- nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
264
+ import { z as z5 } from "zod";
265
+ var inputSchema5 = {
266
+ nicheId: z5.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
249
267
  };
250
268
  var getRankingJuiceTool = {
251
269
  name: "get_ranking_juice",
252
270
  title: "Get Ranking Juices for a Niche",
253
271
  description: "Use this when the user asks 'how do I rank higher', 'what's my ranking juice', or wants listing-optimization guidance. Retrieves the Ranking Juices for each Competitor within the specified Niche. Ranking Juice is DataDive's proprietary metric identifying the key factors driving search-ranking success for each Competitor's listing. Returns currentListing and optimizedListing breakdowns (rankingJuice + per-property contributions: title, bullets, description) plus per-competitor listing scores and the latestResearchDate.",
254
- inputSchema: inputSchema4,
272
+ inputSchema: inputSchema5,
255
273
  handler: async (args, ctx) => {
256
274
  return await httpGet(
257
275
  { config: ctx.config, toolName: "get_ranking_juice" },
@@ -261,26 +279,46 @@ var getRankingJuiceTool = {
261
279
  };
262
280
 
263
281
  // src/tools/list-rank-radars.ts
264
- import { z as z5 } from "zod";
282
+ import { z as z6 } from "zod";
265
283
 
266
284
  // src/types/api.ts
285
+ var ISO_8601 = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
286
+ var SUPPORTED_MARKETPLACES = [
287
+ "com",
288
+ "ca",
289
+ "co.uk",
290
+ "com.mx",
291
+ "in",
292
+ "fr",
293
+ "de",
294
+ "es",
295
+ "it",
296
+ "co.jp"
297
+ ];
267
298
  var RANK_RADAR_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED"];
299
+ var ALERT_STATUSES = ["active", "resolved", "all"];
300
+ var BILLABLE_FEATURE_TYPES = [
301
+ "DIVED_ASINS",
302
+ "PRODUCT_BRIEF_ASINS",
303
+ "AI_COPYWRITER_PROMPTS",
304
+ "RANK_RADAR_KEYWORDS"
305
+ ];
268
306
 
269
307
  // src/tools/list-rank-radars.ts
270
- var inputSchema5 = {
271
- currentPage: z5.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
272
- pageSize: z5.number().int().min(1).max(100).optional().describe("Items per page (max 100). Defaults to 20."),
273
- nicheId: z5.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
274
- status: z5.enum(RANK_RADAR_STATUSES).optional().describe(
308
+ var inputSchema6 = {
309
+ currentPage: z6.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
310
+ pageSize: z6.number().int().min(1).max(100).optional().describe("Items per page (max 100). Defaults to 20."),
311
+ nicheId: z6.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
312
+ status: z6.enum(RANK_RADAR_STATUSES).optional().describe(
275
313
  "Filter by Rank Radar status. By default, returns only ACTIVE Rank Radars. Allowed: ACTIVE, PAUSED, ARCHIVED."
276
314
  ),
277
- searchText: z5.string().optional().describe("Filter Rank Radars by ASIN or product title.")
315
+ searchText: z6.string().optional().describe("Filter Rank Radars by ASIN or product title.")
278
316
  };
279
317
  var listRankRadarsTool = {
280
318
  name: "list_rank_radars",
281
319
  title: "List Rank Radars",
282
320
  description: "Use this to find a `rankRadarId` before calling `get_rank_radar_data`. Filter by `nicheId` if the user has already identified a niche. Retrieves a paginated list of Rank Radars \u2014 keyword-rank trackers monitoring organic and sponsored positions for specific ASINs over time. Each item includes id, asin (with id, krtId, asin), marketplace, keywordCount, title, imageUrl, and summary metrics: top10KW, top10SV, top50KW, top50SV. Supports pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
283
- inputSchema: inputSchema5,
321
+ inputSchema: inputSchema6,
284
322
  handler: async (args, ctx) => {
285
323
  return await httpGet(
286
324
  { config: ctx.config, toolName: "list_rank_radars" },
@@ -297,18 +335,18 @@ var listRankRadarsTool = {
297
335
  };
298
336
 
299
337
  // src/tools/get-rank-radar-data.ts
300
- import { z as z6 } from "zod";
338
+ import { z as z7 } from "zod";
301
339
  var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
302
- var inputSchema6 = {
303
- rankRadarId: z6.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
304
- startDate: z6.string().regex(ISO_DATE, "startDate must be in yyyy-mm-dd format").describe("Start date for the ranking data range, yyyy-mm-dd (e.g. 2024-03-26)."),
305
- endDate: z6.string().regex(ISO_DATE, "endDate must be in yyyy-mm-dd format").describe("End date for the ranking data range, yyyy-mm-dd (e.g. 2024-04-26). Must be on or after startDate.")
340
+ var inputSchema7 = {
341
+ rankRadarId: z7.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
342
+ startDate: z7.string().regex(ISO_DATE, "startDate must be in yyyy-mm-dd format").describe("Start date for the ranking data range, yyyy-mm-dd (e.g. 2024-03-26)."),
343
+ endDate: z7.string().regex(ISO_DATE, "endDate must be in yyyy-mm-dd format").describe("End date for the ranking data range, yyyy-mm-dd (e.g. 2024-04-26). Must be on or after startDate.")
306
344
  };
307
345
  var getRankRadarDataTool = {
308
346
  name: "get_rank_radar_data",
309
347
  title: "Get Keyword Rankings for a Rank Radar",
310
348
  description: "Use this to analyze keyword ranking trends over time. Requires startDate and endDate (yyyy-mm-dd). Retrieves historical keyword ranking data for the specified Rank Radar within the date range. Returns an array of tracked keywords; each has id, keyword, searchVolume, ranks (per-day { date, organicRank, impressionRank }), and any highlight annotations. Use after `list_rank_radars` to discover a `rankRadarId`.",
311
- inputSchema: inputSchema6,
349
+ inputSchema: inputSchema7,
312
350
  handler: async (args, ctx) => {
313
351
  return await httpGet(
314
352
  { config: ctx.config, toolName: "get_rank_radar_data" },
@@ -319,13 +357,13 @@ var getRankRadarDataTool = {
319
357
  };
320
358
 
321
359
  // src/tools/get-asin-inventory-distribution.ts
322
- import { z as z7 } from "zod";
323
- var inputSchema7 = {
324
- sellerId: z7.string().min(1).describe(
360
+ import { z as z8 } from "zod";
361
+ var inputSchema8 = {
362
+ sellerId: z8.string().min(1).describe(
325
363
  "Amazon seller account ID as connected to DataDive. Find it on the Connections page at https://2.datadive.tools \u2014 there is no MCP discovery tool for sellers yet."
326
364
  ),
327
- marketplace: z7.enum(["com", "ca", "co.uk", "com.mx", "in", "fr", "de", "es", "it", "co.jp"]).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
328
- asin: z7.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
365
+ marketplace: z8.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
366
+ asin: z8.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
329
367
  "Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234)."
330
368
  )
331
369
  };
@@ -333,7 +371,7 @@ var getAsinInventoryDistributionTool = {
333
371
  name: "get_asin_inventory_distribution",
334
372
  title: "Get Per-Fulfillment-Center Inventory Distribution for an ASIN",
335
373
  description: "Use this when the user asks about inventory levels, stock by fulfillment center, or where their units are sitting across Amazon's fulfillment network for a specific ASIN. Requires the Amazon sellerId from the user's DataDive Connections page (https://2.datadive.tools), the marketplace code, and the ASIN. Returns totalSellableUnits and a per-FC `distribution` array (fc, state, availableStock, availableStockPercentage). `lastUpdatedAt` may be null when no successful inventory ingestion has occurred in the last 30 days.",
336
- inputSchema: inputSchema7,
374
+ inputSchema: inputSchema8,
337
375
  handler: async (args, ctx) => {
338
376
  return await httpGet(
339
377
  { config: ctx.config, toolName: "get_asin_inventory_distribution" },
@@ -344,15 +382,122 @@ var getAsinInventoryDistributionTool = {
344
382
  }
345
383
  };
346
384
 
385
+ // src/tools/alert-query.ts
386
+ import { z as z9 } from "zod";
387
+ var alertsQueryInputSchema = {
388
+ sellerId: z9.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
389
+ marketplace: z9.enum(SUPPORTED_MARKETPLACES).optional().describe('Filter to a single Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
390
+ status: z9.enum(ALERT_STATUSES).optional().describe(
391
+ "Lifecycle filter. `active` (default) returns unresolved alerts, `resolved` returns resolved alerts, `all` returns both. Dismissed alerts are never returned."
392
+ ),
393
+ updatedSince: z9.string().regex(ISO_8601, "updatedSince must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe(
394
+ "ISO-8601 timestamp; return only alerts surfaced at or after this time (filters on `lastAlertedAt`). Use it to incrementally sync since your last poll. Defaults to the last 30 days when omitted."
395
+ ),
396
+ currentPage: z9.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
397
+ pageSize: z9.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
398
+ };
399
+
400
+ // src/tools/list-indexing-issue-alerts.ts
401
+ var listIndexingIssueAlertsTool = {
402
+ name: "list_indexing_issue_alerts",
403
+ title: "List Indexing-Issue Alerts",
404
+ description: "Use this to find products that may have lost search visibility. Retrieves a paginated list of indexing-issue alerts across the user's connected Amazon seller accounts \u2014 an alert fires when one of their ASINs is no longer indexed for its tracked keywords. By default returns active (unresolved) alerts from the last 30 days; filter by `sellerId`, `marketplace`, `status`, or `updatedSince` (for incremental polling). Each item includes id, asin, title, imageUrl, isParent, sellerId, marketplace, lastAlertedAt, and resolvedAt. Supports pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
405
+ inputSchema: alertsQueryInputSchema,
406
+ handler: async (args, ctx) => {
407
+ return await httpGet(
408
+ { config: ctx.config, toolName: "list_indexing_issue_alerts" },
409
+ "/v1/alerts/indexing-issues",
410
+ {
411
+ sellerId: args.sellerId,
412
+ marketplace: args.marketplace,
413
+ status: args.status,
414
+ updatedSince: args.updatedSince,
415
+ currentPage: args.currentPage,
416
+ pageSize: args.pageSize
417
+ }
418
+ );
419
+ }
420
+ };
421
+
422
+ // src/tools/list-blind-spend-alerts.ts
423
+ var listBlindSpendAlertsTool = {
424
+ name: "list_blind_spend_alerts",
425
+ title: "List Blind-Spend (Wasted-Spend) Alerts",
426
+ description: "Use this to find wasted PPC ad spend. Retrieves a paginated list of blind-spend alerts across the user's connected Amazon seller accounts \u2014 an alert flags ad spend on customer search terms that produced little or no sales. By default returns active (unresolved) alerts from the last 30 days; filter by `sellerId`, `marketplace`, `status`, or `updatedSince` (for incremental polling). Each item includes id, asin, title, imageUrl, sellerId, marketplace, lastAlertedAt, resolvedAt, wastedSpend (total ad spend across unresolved terms), totalKeywordCount, unresolvedKeywordCount, and searchTerms \u2014 the unresolved wasted-spend search terms, each with term, spend, sales, clicks, cvr (0-1), and impressions. Supports pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
427
+ inputSchema: alertsQueryInputSchema,
428
+ handler: async (args, ctx) => {
429
+ return await httpGet(
430
+ { config: ctx.config, toolName: "list_blind_spend_alerts" },
431
+ "/v1/alerts/blind-spend",
432
+ {
433
+ sellerId: args.sellerId,
434
+ marketplace: args.marketplace,
435
+ status: args.status,
436
+ updatedSince: args.updatedSince,
437
+ currentPage: args.currentPage,
438
+ pageSize: args.pageSize
439
+ }
440
+ );
441
+ }
442
+ };
443
+
444
+ // src/tools/get-quota.ts
445
+ var inputSchema9 = {};
446
+ var getQuotaTool = {
447
+ name: "get_quota",
448
+ title: "Get Quota Usage",
449
+ description: "Use this when the user asks how much quota or how many tokens they have left, or before creating a Niche Dive or Rank Radar to check remaining headroom. Returns current quota usage and capacity for each billable feature \u2014 DIVED_ASINS (Dive tokens), RANK_RADAR_KEYWORDS (tracked keywords), PRODUCT_BRIEF_ASINS, and AI_COPYWRITER_PROMPTS \u2014 each with `used` and `capacity` (null when unlimited / not applicable), plus `nextRefreshDate` (ISO-8601 timestamp of the next quota reset, or null). Takes no arguments.",
450
+ inputSchema: inputSchema9,
451
+ handler: async (_args, ctx) => {
452
+ return await httpGet({ config: ctx.config, toolName: "get_quota" }, "/v1/quota");
453
+ }
454
+ };
455
+
456
+ // src/tools/list-usage.ts
457
+ import { z as z10 } from "zod";
458
+ var inputSchema10 = {
459
+ type: z10.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
460
+ search: z10.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
461
+ startDate: z10.string().regex(ISO_8601, "startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)").optional().describe("ISO-8601 date/timestamp; return only usage logs recorded on or after this time."),
462
+ endDate: z10.string().regex(ISO_8601, "endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)").optional().describe("ISO-8601 date/timestamp; return only usage logs recorded on or before this time."),
463
+ currentPage: z10.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
464
+ pageSize: z10.number().int().min(1).max(200).optional().describe("Items per page (max 200). Defaults to 50.")
465
+ };
466
+ var listUsageTool = {
467
+ name: "list_usage",
468
+ title: "List Billable Usage Logs",
469
+ description: "Use this when the user asks who consumed tokens, how their quota was spent, or wants an audit of billable activity over a date range. Retrieves a paginated list of billable feature usage logs for the organization \u2014 each entry is a token-consumption event (a dive, rank-radar creation, AI copywriter prompt, etc.). Filter by `type` (billable feature), `search` (user name/email), and `startDate`/`endDate`. Each item includes name, email, qty (tokens consumed), type, action (specific operation), nicheId/nicheName, rankRadarId, and date, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
470
+ inputSchema: inputSchema10,
471
+ handler: async (args, ctx) => {
472
+ return await httpGet(
473
+ { config: ctx.config, toolName: "list_usage" },
474
+ "/v1/usage",
475
+ {
476
+ type: args.type,
477
+ search: args.search,
478
+ startDate: args.startDate,
479
+ endDate: args.endDate,
480
+ currentPage: args.currentPage,
481
+ pageSize: args.pageSize
482
+ }
483
+ );
484
+ }
485
+ };
486
+
347
487
  // src/tools/index.ts
348
488
  var allTools = [
349
489
  listNichesTool,
350
490
  getNicheKeywordsTool,
491
+ getNicheRootsTool,
351
492
  getNicheCompetitorsTool,
352
493
  getRankingJuiceTool,
353
494
  listRankRadarsTool,
354
495
  getRankRadarDataTool,
355
- getAsinInventoryDistributionTool
496
+ getAsinInventoryDistributionTool,
497
+ listIndexingIssueAlertsTool,
498
+ listBlindSpendAlertsTool,
499
+ getQuotaTool,
500
+ listUsageTool
356
501
  ];
357
502
 
358
503
  // src/server.ts
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/server.ts","../src/tools/list-niches.ts","../src/http/errors.ts","../package.json","../src/http/client.ts","../src/tools/get-niche-keywords.ts","../src/tools/get-niche-competitors.ts","../src/tools/get-ranking-juice.ts","../src/tools/list-rank-radars.ts","../src/types/api.ts","../src/tools/get-rank-radar-data.ts","../src/tools/get-asin-inventory-distribution.ts","../src/tools/index.ts"],"sourcesContent":["/**\n * Entry point for the datadive-mcp stdio binary.\n *\n * Wired via package.json#bin -> dist/index.js so `npx -y @datadive-tools/mcp` runs it.\n * The shebang is added at build time by tsup.config.ts (banner.js).\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { loadConfig } from \"./config.js\";\nimport { buildServer } from \"./server.js\";\n\nasync function main() {\n let config;\n try {\n config = loadConfig();\n } catch (err) {\n // Config errors (e.g. missing API key) are user-facing; print to stderr and exit\n // non-zero so the MCP client surfaces a real error rather than silently disconnecting.\n const msg = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[datadive-mcp] ${msg}\\n`);\n process.exit(1);\n }\n\n const server = buildServer(config);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err) => {\n const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);\n process.stderr.write(`[datadive-mcp] fatal: ${msg}\\n`);\n process.exit(1);\n});\n","/**\n * Loads and validates the server's runtime configuration from environment.\n *\n * Required:\n * DATADIVE_API_KEY - your DataDive API key (generate at 2.datadive.tools/api-key)\n *\n * Optional:\n * DATADIVE_API_BASE_URL - override the API base URL (defaults to https://api.datadive.tools)\n */\n\nexport interface Config {\n apiKey: string;\n baseUrl: string;\n}\n\nconst DEFAULT_BASE_URL = \"https://api.datadive.tools\";\nconst KEY_HELP_URL = \"https://2.datadive.tools/api-key\";\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const apiKey = (env.DATADIVE_API_KEY ?? \"\").trim();\n if (!apiKey) {\n throw new Error(\n `DATADIVE_API_KEY environment variable is required. Generate one at ${KEY_HELP_URL} ` +\n `and add it to your MCP client config under env.DATADIVE_API_KEY.`,\n );\n }\n\n const rawBase = (env.DATADIVE_API_BASE_URL ?? DEFAULT_BASE_URL).trim();\n // Normalize: strip trailing slashes so we can append \"/v1/...\" paths cleanly.\n const baseUrl = rawBase.replace(/\\/+$/, \"\");\n\n return { apiKey, baseUrl };\n}\n","/**\n * Builds the MCP server: registers each tool from the registry with the SDK.\n *\n * Error handling: if a tool handler throws (most commonly an ApiError from the\n * HTTP client), we surface the message back through MCP as a tool-level error\n * (`isError: true`). The LLM sees the message and can react — e.g. tell the\n * user to regenerate their API key on a 401.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { Config } from \"./config.js\";\nimport { allTools } from \"./tools/index.js\";\nimport { ApiError } from \"./http/errors.js\";\nimport { PKG_VERSION, takeUpgradeNotice } from \"./http/client.js\";\n\nexport function buildServer(config: Config): McpServer {\n const server = new McpServer({\n name: \"datadive\",\n version: PKG_VERSION,\n });\n\n for (const tool of allTools) {\n server.registerTool(\n tool.name,\n {\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema,\n },\n async (args: Record<string, unknown>) => {\n try {\n const data = await tool.handler(args, { config });\n const content = [\n {\n type: \"text\" as const,\n text: JSON.stringify(data, null, 2),\n },\n ];\n // If the backend signaled a newer release, append a one-time upgrade\n // nudge so the model relays it to the user. takeUpgradeNotice() returns\n // null after the first time, so this fires at most once per session.\n const notice = takeUpgradeNotice();\n if (notice) {\n content.push({\n type: \"text\" as const,\n text: `⚠️ datadive-mcp update available — ${notice}`,\n });\n }\n return { content };\n } catch (err) {\n return {\n isError: true,\n content: [\n {\n type: \"text\" as const,\n text: err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err),\n },\n ],\n };\n }\n },\n );\n }\n\n return server;\n}\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { NicheList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n currentPage: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Page number, 1-indexed. Defaults to 1.\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Items per page (max 100). Defaults to 20.\"),\n};\n\nexport const listNichesTool: ToolDefinition<typeof inputSchema> = {\n name: \"list_niches\",\n title: \"List DataDive Niches\",\n description:\n \"Use this first when the user asks about their niches, or to find a `nicheId` for use with \" +\n \"`get_niche_keywords`, `get_niche_competitors`, or `get_ranking_juice`. \" +\n \"Retrieves a paginated list of Niches. Each Niche represents a market segment or product \" +\n \"category being tracked. Returns nicheId, heroKeyword, nicheLabel, marketplace (com/uk/de/...), \" +\n \"and latestResearchDate per niche, plus pagination metadata (currentPage, pageSize, total, \" +\n \"lastPage, hasNext, hasPrev).\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<NicheList>(\n { config: ctx.config, toolName: \"list_niches\" },\n \"/v1/niches\",\n { currentPage: args.currentPage, pageSize: args.pageSize },\n );\n },\n};\n","/**\n * Error mapping for the DataDive /v1 API.\n *\n * The MCP server is a thin HTTP shim — when api.datadive.tools returns an error,\n * we want the LLM (and ultimately the user) to see a useful, actionable message\n * rather than \"Request failed\". `ApiError.fromHttp` turns an HTTP status + body\n * into one of those messages.\n *\n * Source of truth for status semantics:\n * datadive-backend/src/external-api/auth/api-key.guard.ts (401/402 logic)\n * datadive-backend/src/external-api/external-api-v1.controller.ts (per-route docs)\n */\n\nconst KEY_HELP_URL = \"https://2.datadive.tools/api-key\";\n\nexport type ApiErrorKind =\n | \"auth\"\n | \"payment\"\n | \"forbidden\"\n | \"not_found\"\n | \"bad_request\"\n | \"rate_limit\"\n | \"server\"\n | \"network\"\n | \"http\";\n\nexport class ApiError extends Error {\n public readonly kind: ApiErrorKind;\n public readonly status: number;\n public readonly body: unknown;\n\n constructor(kind: ApiErrorKind, status: number, message: string, body?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.kind = kind;\n this.status = status;\n this.body = body;\n }\n\n static fromHttp(status: number, body: unknown): ApiError {\n const serverMsg = extractMessage(body);\n\n if (status === 400) {\n return new ApiError(\n \"bad_request\",\n 400,\n serverMsg ? `Bad request: ${serverMsg}` : \"Bad request: invalid parameters.\",\n body,\n );\n }\n if (status === 401) {\n return new ApiError(\n \"auth\",\n 401,\n `Authentication failed: your DATADIVE_API_KEY is invalid or expired. Generate a new key at ${KEY_HELP_URL}.`,\n body,\n );\n }\n if (status === 402) {\n return new ApiError(\n \"payment\",\n 402,\n \"Subscription is inactive or paused. Resume billing at https://2.datadive.tools to use the API.\",\n body,\n );\n }\n if (status === 403) {\n return new ApiError(\n \"forbidden\",\n 403,\n serverMsg\n ? `Forbidden: ${serverMsg}`\n : \"Forbidden: this API key cannot access the requested resource.\",\n body,\n );\n }\n if (status === 404) {\n return new ApiError(\"not_found\", 404, serverMsg ? `Not found: ${serverMsg}` : \"Resource not found.\", body);\n }\n if (status === 429) {\n return new ApiError(\"rate_limit\", 429, \"Rate limit exceeded. Wait a few seconds and retry.\", body);\n }\n if (status >= 500) {\n return new ApiError(\n \"server\",\n status,\n `DataDive API error (${status}). ${serverMsg ?? \"Please try again.\"}`.trim(),\n body,\n );\n }\n return new ApiError(\"http\", status, serverMsg ?? `HTTP ${status}`, body);\n }\n}\n\nfunction extractMessage(body: unknown): string | null {\n if (!body || typeof body !== \"object\") return null;\n const b = body as Record<string, unknown>;\n // NestJS conventional error shapes: { message: string | string[] } or { error, message }.\n if (typeof b.message === \"string\") return b.message;\n if (Array.isArray(b.message) && b.message.every((m) => typeof m === \"string\")) {\n return (b.message as string[]).join(\"; \");\n }\n if (typeof b.error === \"string\") return b.error;\n return null;\n}\n","{\n \"name\": \"@datadive-tools/mcp\",\n \"version\": \"0.3.0\",\n \"description\": \"MCP server for DataDive — query your niches, keywords, competitors, and Rank Radar data from Claude Desktop, Claude Code, or Cursor.\",\n \"type\": \"module\",\n \"bin\": {\n \"datadive-mcp\": \"./dist/index.js\"\n },\n \"main\": \"./dist/index.js\",\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"test:smoke\": \"DATADIVE_SMOKE=1 vitest run test/smoke.test.ts\",\n \"lint\": \"eslint .\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepublishOnly\": \"npm run lint && npm run typecheck && npm run test && npm run build\"\n },\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"datadive\",\n \"amazon\",\n \"claude\"\n ],\n \"author\": \"DataDive\",\n \"license\": \"MIT\",\n \"homepage\": \"https://github.com/Data-Dive-Tools/datadive-mcp\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Data-Dive-Tools/datadive-mcp.git\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/Data-Dive-Tools/datadive-mcp/issues\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.0.0\",\n \"zod\": \"^3.23.0\"\n },\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.27.0\",\n \"@types/node\": \"^20.0.0\",\n \"@typescript-eslint/eslint-plugin\": \"^8.0.0\",\n \"@typescript-eslint/parser\": \"^8.0.0\",\n \"eslint\": \"^9.0.0\",\n \"prettier\": \"^3.0.0\",\n \"tsup\": \"^8.0.0\",\n \"typescript\": \"~5.9.0\",\n \"vitest\": \"^2.0.0\"\n }\n}\n","/**\n * HTTP client for the DataDive /v1 API.\n *\n * Responsibilities:\n * 1. Attach `x-api-key` from config to every request.\n * 2. Tag the User-Agent with the package version + tool name so backend access\n * logs can attribute usage per tool (no separate telemetry endpoint needed).\n * 3. Map HTTP errors via `ApiError.fromHttp`.\n * 4. Unwrap the `ResponseDto<T>` envelope (`{ message?, success?, data }`)\n * while preserving bare `PaginationResponse<T>` bodies (which need their\n * pagination metadata to reach the LLM).\n *\n * Envelope detection (verified against datadive-backend):\n * - GET /v1/niches -> NicheList extends PaginationResponse<NicheItem> (BARE)\n * - GET /v1/niches/rank-radars -> ResponseDto<ExternalRankRadarListResponseDto>\n * where the inner DTO IS itself paginated.\n * - All other read endpoints -> ResponseDto<T> (UNWRAP)\n *\n * Heuristic: if the body has `data` AND any pagination key (currentPage, lastPage,\n * hasNext, hasPrev, pageSize, total), it's a PaginationResponse — return as-is.\n * Otherwise if the body has `data`, it's a ResponseDto envelope — return body.data.\n *\n * Cite: datadive-backend/src/common/pagination/pagination.dto.ts (pagination keys)\n * datadive-backend/src/common/dto/response.dto.ts (envelope shape)\n */\n\nimport type { Config } from \"../config.js\";\nimport { ApiError } from \"./errors.js\";\nimport { version } from \"../../package.json\";\n\n/**\n * The running package version, inlined from package.json at build time by\n * tsup/esbuild (and by vite under vitest). Single source of truth — no manual\n * bump here, so it can never drift from what's published to npm.\n *\n * Written into the User-Agent header, and compared against the backend's\n * advertised latest version (see noteLatestVersion) to nudge stale installs.\n */\nexport const PKG_VERSION: string = version;\n\n/** Response header the DataDive backend uses to advertise the latest published MCP version. */\nconst LATEST_VERSION_HEADER = \"x-datadive-mcp-latest\";\n\n/**\n * One-time upgrade notice. Armed when the backend advertises a version newer than\n * the one we're running; consumed (and then suppressed for the rest of the session)\n * by takeUpgradeNotice(). We surface it once per process so we don't nag on every call.\n */\nlet pendingUpgradeNotice: string | null = null;\nlet hasWarned = false;\n\n/**\n * Compares dotted version strings (major.minor.patch). Pre-release / build suffixes\n * are stripped — a stable release is the signal we act on. Returns true iff `latest`\n * is strictly newer than `current`. Exported for unit testing.\n */\nexport function isNewerVersion(latest: string, current: string): boolean {\n const parse = (v: string) => v.split(/[-+]/)[0]!.split(\".\").map((n) => parseInt(n, 10) || 0);\n const a = parse(latest);\n const b = parse(current);\n for (let i = 0; i < 3; i++) {\n const da = a[i] ?? 0;\n const db = b[i] ?? 0;\n if (da !== db) return da > db;\n }\n return false;\n}\n\n/** Records the advertised latest version; arms a one-time notice if we're behind. Never throws. */\nfunction noteLatestVersion(latest: string | null): void {\n if (!latest || hasWarned) return;\n if (isNewerVersion(latest, PKG_VERSION)) {\n pendingUpgradeNotice =\n `A newer version of datadive-mcp is available (${latest}; you are running ${PKG_VERSION}). ` +\n `Update by reinstalling the latest from npm — e.g. \\`npm install -g @datadive-tools/mcp@latest\\` — ` +\n `or bump the @datadive-tools/mcp version in your MCP client config, then restart the client.`;\n }\n}\n\n/**\n * Returns a pending upgrade notice once, then suppresses further notices for the rest\n * of the session. Called when assembling a tool result so the LLM can relay it to the\n * user. Returns null when there's nothing to surface.\n */\nexport function takeUpgradeNotice(): string | null {\n if (!pendingUpgradeNotice) return null;\n const notice = pendingUpgradeNotice;\n pendingUpgradeNotice = null;\n hasWarned = true;\n return notice;\n}\n\nconst PAGINATION_KEYS = [\"currentPage\", \"lastPage\", \"hasNext\", \"hasPrev\", \"pageSize\", \"total\"] as const;\n\nexport interface RequestContext {\n config: Config;\n /** Tool name used to tag the User-Agent header for per-tool log attribution. */\n toolName: string;\n}\n\nexport async function httpGet<T>(ctx: RequestContext, path: string, query?: Record<string, unknown>): Promise<T> {\n const url = buildUrl(ctx.config.baseUrl, path, query);\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"GET\",\n headers: {\n \"x-api-key\": ctx.config.apiKey,\n accept: \"application/json\",\n \"user-agent\": userAgent(ctx.toolName),\n },\n });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n throw new ApiError(\"network\", 0, `Network error reaching ${url}: ${msg}`);\n }\n\n // The backend advertises the latest published MCP version on every response\n // (including errors); arm a one-time upgrade notice if we're behind.\n noteLatestVersion(res.headers.get(LATEST_VERSION_HEADER));\n\n const text = await res.text();\n let body: unknown;\n try {\n body = text.length > 0 ? JSON.parse(text) : null;\n } catch {\n body = text;\n }\n\n if (!res.ok) {\n throw ApiError.fromHttp(res.status, body);\n }\n\n return unwrap<T>(body);\n}\n\nfunction buildUrl(baseUrl: string, path: string, query?: Record<string, unknown>): string {\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n const u = new URL(`${baseUrl}${normalizedPath}`);\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined || v === null) continue;\n u.searchParams.set(k, String(v));\n }\n }\n return u.toString();\n}\n\nfunction userAgent(toolName: string): string {\n return `datadive-mcp/${PKG_VERSION} tool/${toolName}`;\n}\n\n/**\n * Unwraps a ResponseDto<T> envelope while preserving bare PaginationResponse bodies.\n * Exported for unit testing.\n */\nexport function unwrap<T>(body: unknown): T {\n if (!isObject(body)) return body as T;\n if (!(\"data\" in body)) return body as T;\n // If pagination metadata is present at the top level, return as-is.\n for (const key of PAGINATION_KEYS) {\n if (key in body) return body as T;\n }\n // Plain ResponseDto envelope -> unwrap.\n return (body as { data: T }).data;\n}\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { GetMasterKeywordListResult } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getNicheKeywordsTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_niche_keywords\",\n title: \"Get Master Keyword List for a Niche\",\n description:\n \"Use this when the user asks about keywords, search terms, or search volume for a niche. \" +\n \"Retrieves the master keyword list for the specified Niche — the relevant search terms \" +\n \"monitored for ranking and performance metrics. For each keyword, returns search volume, \" +\n \"relevancy (numeric score or \\\"Outlier\\\"), and competitor ASIN organic ranks \" +\n \"(asinRanks: { ASIN -> rank | null }). Also returns the latestResearchDate.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<GetMasterKeywordListResult>(\n { config: ctx.config, toolName: \"get_niche_keywords\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { GetCompetitorListResult } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getNicheCompetitorsTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_niche_competitors\",\n title: \"Get Competitors and Niche Statistics\",\n description:\n \"Use this for competitor ASINs, sales/revenue benchmarks, or niche opportunity scoring. \" +\n \"Retrieves the list of Competitors within the specified Niche along with Niche statistics. \" +\n \"For the Niche: keyword statistics, opportunity evaluation, benchmark median values, and \" +\n \"overall competitor strength assessment. For each Competitor: ASIN, sales, revenue, ratings, \" +\n \"reviews, price, image URL, and ranking data.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<GetCompetitorListResult>(\n { config: ctx.config, toolName: \"get_niche_competitors\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/competitors`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { NicheRankingJuices } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getRankingJuiceTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_ranking_juice\",\n title: \"Get Ranking Juices for a Niche\",\n description:\n \"Use this when the user asks 'how do I rank higher', 'what's my ranking juice', or wants \" +\n \"listing-optimization guidance. \" +\n \"Retrieves the Ranking Juices for each Competitor within the specified Niche. Ranking Juice is \" +\n \"DataDive's proprietary metric identifying the key factors driving search-ranking success \" +\n \"for each Competitor's listing. Returns currentListing and optimizedListing breakdowns \" +\n \"(rankingJuice + per-property contributions: title, bullets, description) plus per-competitor \" +\n \"listing scores and the latestResearchDate.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<NicheRankingJuices>(\n { config: ctx.config, toolName: \"get_ranking_juice\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/ranking-juices`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport { RANK_RADAR_STATUSES, type RankRadarList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n currentPage: z.number().int().min(1).optional().describe(\"Page number, 1-indexed. Defaults to 1.\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Items per page (max 100). Defaults to 20.\"),\n nicheId: z\n .string()\n .min(1)\n .optional()\n .describe(\"Filter Rank Radars by Niche identifier. Use after `list_niches`.\"),\n status: z\n .enum(RANK_RADAR_STATUSES)\n .optional()\n .describe(\n \"Filter by Rank Radar status. By default, returns only ACTIVE Rank Radars. Allowed: ACTIVE, PAUSED, ARCHIVED.\",\n ),\n searchText: z.string().optional().describe(\"Filter Rank Radars by ASIN or product title.\"),\n};\n\nexport const listRankRadarsTool: ToolDefinition<typeof inputSchema> = {\n name: \"list_rank_radars\",\n title: \"List Rank Radars\",\n description:\n \"Use this to find a `rankRadarId` before calling `get_rank_radar_data`. Filter by `nicheId` \" +\n \"if the user has already identified a niche. \" +\n \"Retrieves a paginated list of Rank Radars — keyword-rank trackers monitoring organic and \" +\n \"sponsored positions for specific ASINs over time. Each item includes id, asin (with id, krtId, \" +\n \"asin), marketplace, keywordCount, title, imageUrl, and summary metrics: top10KW, top10SV, \" +\n \"top50KW, top50SV. Supports pagination metadata (currentPage, pageSize, total, lastPage, \" +\n \"hasNext, hasPrev).\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<RankRadarList>(\n { config: ctx.config, toolName: \"list_rank_radars\" },\n \"/v1/niches/rank-radars\",\n {\n currentPage: args.currentPage,\n pageSize: args.pageSize,\n nicheId: args.nicheId,\n status: args.status,\n searchText: args.searchText,\n },\n );\n },\n};\n","/**\n * Hand-mirrored DTOs from the DataDive backend (v1 external API).\n *\n * Source of truth (in this priority order):\n * 1. Live Swagger: https://developer.datadive.tools/docs#/v1\n * (machine-readable: https://developer.datadive.tools/docs-json)\n * 2. Backend DTOs: datadive-backend/src/external-api/dto/*.ts\n * datadive-backend/src/niche-research/dto/rank-radar.response.dto.ts\n * datadive-backend/src/common/dto/response.dto.ts\n * datadive-backend/src/common/pagination/pagination.dto.ts\n * 3. Controller: datadive-backend/src/external-api/external-api-v1.controller.ts\n *\n * Last synced: 2026-05-05.\n *\n * The MCP server forwards JSON straight to the LLM, so deeply-nested DTOs are\n * intentionally typed loosely (with `unknown` or pass-through Records) where\n * the TS surface doesn't add safety. Pin tighter as use cases require.\n */\n\n// ─── Envelope + pagination ───────────────────────────────────────────────────\n\n/** Generic NestJS response envelope: { message?, success?, data? }. */\nexport interface ResponseEnvelope<T> {\n message?: string;\n /** @deprecated — use HTTP status as the success indicator. */\n success?: boolean;\n data?: T | null;\n}\n\n/** Standard pagination envelope returned by list endpoints. */\nexport interface PaginationResponse<T> {\n data: T[];\n currentPage: number;\n pageSize: number;\n total: number;\n lastPage: number;\n hasNext: boolean;\n hasPrev: boolean;\n}\n\n// ─── /v1/niches (NicheList = PaginationResponse<NicheItem>) ─────────────────\n\nexport interface NicheItem {\n nicheId: string;\n heroKeyword: string;\n nicheLabel: string;\n /** Amazon marketplace TLD: \"com\", \"uk\", \"de\", etc. */\n marketplace: string;\n /** ISO date string when the Niche was last researched. Nullable. */\n latestResearchDate: string | null;\n}\n\nexport type NicheList = PaginationResponse<NicheItem>;\n\n// ─── /v1/niches/:nicheId/keywords (GetMasterKeywordListResult, wrapped) ─────\n\n/** Map of competitor ASIN -> rank (or null when unranked). */\nexport type AsinRanks = Record<string, number | null>;\n\nexport interface MasterKeyword {\n keyword: string;\n searchVolume: number;\n /** \"Outlier\" or a numeric relevancy score. */\n relevancy: string | number;\n asinRanks: AsinRanks;\n}\n\nexport interface GetMasterKeywordListResult {\n keywords: MasterKeyword[];\n /** ISO date string. */\n latestResearchDate: string;\n}\n\n// ─── /v1/niches/:nicheId/competitors (GetCompetitorListResult, wrapped) ─────\n\n/**\n * Niche competitors response — many nested DTOs from the backend (MklStatisticsData,\n * OpportunityEvaluation, MklBenchmark, CompetitorsStrength, AsinListItem). We type\n * the outer shape and leave the inner objects as opaque records; the LLM sees the\n * full JSON and can reason about it. Tighten if a specific consumer needs it.\n */\nexport interface GetCompetitorListResult {\n /** Marketplace enum (AmazonStore in backend). */\n marketplace: string;\n statistics: Record<string, unknown>;\n opportunityEvaluation: Record<string, unknown>;\n benchmark: Record<string, unknown>;\n competitorsStrength: Record<string, unknown>;\n competitors: Array<Record<string, unknown>>;\n latestResearchDate: string;\n}\n\n// ─── /v1/niches/:nicheId/ranking-juices (NicheRankingJuices, wrapped) ───────\n\nexport interface RankingJuiceProperty {\n rankingJuice: number;\n}\n\nexport interface ListingRankingJuice {\n rankingJuice: number;\n title: RankingJuiceProperty;\n bullets: RankingJuiceProperty;\n description: RankingJuiceProperty;\n}\n\nexport interface CompetitorRankingJuice {\n asin: string;\n listing: ListingRankingJuice;\n}\n\nexport interface NicheRankingJuices {\n currentListing: ListingRankingJuice;\n optimizedListing: ListingRankingJuice;\n competitors: CompetitorRankingJuice[];\n latestResearchDate: string;\n}\n\n// ─── /v1/niches/rank-radars (ExternalRankRadarListResponseDto, wrapped) ─────\n\nexport interface RankRadarAsin {\n id: string;\n krtId: string;\n asin: string;\n}\n\nexport interface RankRadarItem {\n id: string;\n asin: RankRadarAsin;\n marketplace: string;\n keywordCount: number;\n title: string;\n imageUrl: string;\n top10KW: number | null;\n top10SV: number | null;\n top50KW: number | null;\n top50SV: number | null;\n}\n\nexport type RankRadarList = PaginationResponse<RankRadarItem>;\n\n/** Allowed values for the `status` query param on /v1/niches/rank-radars. */\nexport const RANK_RADAR_STATUSES = [\"ACTIVE\", \"PAUSED\", \"ARCHIVED\"] as const;\nexport type RankRadarStatus = (typeof RANK_RADAR_STATUSES)[number];\n\n// ─── /v1/niches/rank-radars/:rankRadarId (KrtKeywordResponseDto[], wrapped) ─\n\nexport interface KrtAsinRank {\n /** ISO date string. */\n date: string;\n organicRank?: number | null;\n impressionRank?: number | null;\n}\n\nexport interface KrtKeyword {\n id: string;\n keyword: string;\n searchVolume: number | null;\n ranks: KrtAsinRank[];\n /** Highlight annotations (KrtHighlightDto in backend); kept opaque for MVP. */\n highlights: Array<Record<string, unknown>>;\n}\n\nexport type RankRadarKeywordList = KrtKeyword[];\n\n// ─── /v1/sellers/:sellerId/marketplaces/:marketplace/asins/:asin/inventory ────\n// (InventoryByFcResponseDto, wrapped)\n\nexport interface InventoryByFcItem {\n /** Fulfillment center code. */\n fc: string;\n /** State or region code where the FC is located, when known. */\n state: string;\n /** Units of sellable inventory currently at this FC. May be 0 when stock is out. */\n availableStock: number;\n /** Share of the ASIN's total sellable inventory at this FC, as a 0..1 fraction. */\n availableStockPercentage: number;\n}\n\nexport interface InventoryByFcResponse {\n asin: string;\n sellerId: string;\n /** Marketplace enum: \"com\", \"ca\", \"co.uk\", \"com.mx\", \"in\", \"fr\", \"de\", \"es\", \"it\", \"co.jp\". */\n marketplace: string;\n /** ISO timestamp of last successful ingestion. Null if none in the last 30 days. */\n lastUpdatedAt: string | null;\n totalSellableUnits: number;\n distribution: InventoryByFcItem[];\n}\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { RankRadarKeywordList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\n/** yyyy-mm-dd matcher used by both startDate and endDate. */\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nconst inputSchema = {\n rankRadarId: z\n .string()\n .uuid()\n .describe(\"The Rank Radar UUID (from `list_rank_radars`).\"),\n startDate: z\n .string()\n .regex(ISO_DATE, \"startDate must be in yyyy-mm-dd format\")\n .describe(\"Start date for the ranking data range, yyyy-mm-dd (e.g. 2024-03-26).\"),\n endDate: z\n .string()\n .regex(ISO_DATE, \"endDate must be in yyyy-mm-dd format\")\n .describe(\"End date for the ranking data range, yyyy-mm-dd (e.g. 2024-04-26). Must be on or after startDate.\"),\n};\n\nexport const getRankRadarDataTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_rank_radar_data\",\n title: \"Get Keyword Rankings for a Rank Radar\",\n description:\n \"Use this to analyze keyword ranking trends over time. Requires startDate and endDate (yyyy-mm-dd). \" +\n \"Retrieves historical keyword ranking data for the specified Rank Radar within the date range. \" +\n \"Returns an array of tracked keywords; each has id, keyword, searchVolume, ranks (per-day \" +\n \"{ date, organicRank, impressionRank }), and any highlight annotations. Use after \" +\n \"`list_rank_radars` to discover a `rankRadarId`.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<RankRadarKeywordList>(\n { config: ctx.config, toolName: \"get_rank_radar_data\" },\n `/v1/niches/rank-radars/${encodeURIComponent(args.rankRadarId)}`,\n { startDate: args.startDate, endDate: args.endDate },\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { InventoryByFcResponse } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n sellerId: z\n .string()\n .min(1)\n .describe(\n \"Amazon seller account ID as connected to DataDive. Find it on the Connections page at https://2.datadive.tools — there is no MCP discovery tool for sellers yet.\",\n ),\n marketplace: z\n .enum([\"com\", \"ca\", \"co.uk\", \"com.mx\", \"in\", \"fr\", \"de\", \"es\", \"it\", \"co.jp\"])\n .describe('Amazon marketplace code, e.g. \"com\" for amazon.com or \"co.uk\" for amazon.co.uk.'),\n asin: z\n .string()\n .regex(/^[A-Z0-9]{10}$/, \"ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).\")\n .describe(\n \"Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234).\",\n ),\n};\n\nexport const getAsinInventoryDistributionTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_asin_inventory_distribution\",\n title: \"Get Per-Fulfillment-Center Inventory Distribution for an ASIN\",\n description:\n \"Use this when the user asks about inventory levels, stock by fulfillment center, \" +\n \"or where their units are sitting across Amazon's fulfillment network for a specific ASIN. \" +\n \"Requires the Amazon sellerId from the user's DataDive Connections page \" +\n \"(https://2.datadive.tools), the marketplace code, and the ASIN. Returns totalSellableUnits \" +\n \"and a per-FC `distribution` array (fc, state, availableStock, availableStockPercentage). \" +\n \"`lastUpdatedAt` may be null when no successful inventory ingestion has occurred in the last 30 days.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<InventoryByFcResponse>(\n { config: ctx.config, toolName: \"get_asin_inventory_distribution\" },\n `/v1/sellers/${encodeURIComponent(args.sellerId)}/marketplaces/${encodeURIComponent(\n args.marketplace,\n )}/asins/${encodeURIComponent(args.asin)}/inventory`,\n );\n },\n};\n","/**\n * Tool registry. The order here is the order tools appear in `tools/list`\n * over the MCP transport, which biases LLM discovery — keep \"discovery\" tools\n * (the ones that return IDs needed by other tools) at the top.\n */\n\nimport { listNichesTool } from \"./list-niches.js\";\nimport { getNicheKeywordsTool } from \"./get-niche-keywords.js\";\nimport { getNicheCompetitorsTool } from \"./get-niche-competitors.js\";\nimport { getRankingJuiceTool } from \"./get-ranking-juice.js\";\nimport { listRankRadarsTool } from \"./list-rank-radars.js\";\nimport { getRankRadarDataTool } from \"./get-rank-radar-data.js\";\nimport { getAsinInventoryDistributionTool } from \"./get-asin-inventory-distribution.js\";\nimport type { AnyTool } from \"./types.js\";\n\n// Note on the cast: each ToolDefinition<S> has a handler typed to its specific\n// schema; the registry needs a uniform shape. The MCP SDK validates incoming\n// args against `inputSchema` before calling the handler, so the cast is safe.\nexport const allTools: ReadonlyArray<AnyTool> = [\n listNichesTool,\n getNicheKeywordsTool,\n getNicheCompetitorsTool,\n getRankingJuiceTool,\n listRankRadarsTool,\n getRankRadarDataTool,\n getAsinInventoryDistributionTool,\n] as unknown as ReadonlyArray<AnyTool>;\n\nexport type { ToolDefinition, AnyTool } from \"./types.js\";\n"],"mappings":"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","names":["KEY_HELP_URL","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/server.ts","../src/tools/list-niches.ts","../src/http/errors.ts","../package.json","../src/http/client.ts","../src/tools/get-niche-keywords.ts","../src/tools/get-niche-roots.ts","../src/tools/get-niche-competitors.ts","../src/tools/get-ranking-juice.ts","../src/tools/list-rank-radars.ts","../src/types/api.ts","../src/tools/get-rank-radar-data.ts","../src/tools/get-asin-inventory-distribution.ts","../src/tools/alert-query.ts","../src/tools/list-indexing-issue-alerts.ts","../src/tools/list-blind-spend-alerts.ts","../src/tools/get-quota.ts","../src/tools/list-usage.ts","../src/tools/index.ts"],"sourcesContent":["/**\n * Entry point for the datadive-mcp stdio binary.\n *\n * Wired via package.json#bin -> dist/index.js so `npx -y @datadive-tools/mcp` runs it.\n * The shebang is added at build time by tsup.config.ts (banner.js).\n */\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { loadConfig } from \"./config.js\";\nimport { buildServer } from \"./server.js\";\n\nasync function main() {\n let config;\n try {\n config = loadConfig();\n } catch (err) {\n // Config errors (e.g. missing API key) are user-facing; print to stderr and exit\n // non-zero so the MCP client surfaces a real error rather than silently disconnecting.\n const msg = err instanceof Error ? err.message : String(err);\n process.stderr.write(`[datadive-mcp] ${msg}\\n`);\n process.exit(1);\n }\n\n const server = buildServer(config);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n}\n\nmain().catch((err) => {\n const msg = err instanceof Error ? (err.stack ?? err.message) : String(err);\n process.stderr.write(`[datadive-mcp] fatal: ${msg}\\n`);\n process.exit(1);\n});\n","/**\n * Loads and validates the server's runtime configuration from environment.\n *\n * Required:\n * DATADIVE_API_KEY - your DataDive API key (generate at 2.datadive.tools/api-key)\n *\n * Optional:\n * DATADIVE_API_BASE_URL - override the API base URL (defaults to https://api.datadive.tools)\n */\n\nexport interface Config {\n apiKey: string;\n baseUrl: string;\n}\n\nconst DEFAULT_BASE_URL = \"https://api.datadive.tools\";\nconst KEY_HELP_URL = \"https://2.datadive.tools/api-key\";\n\nexport function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {\n const apiKey = (env.DATADIVE_API_KEY ?? \"\").trim();\n if (!apiKey) {\n throw new Error(\n `DATADIVE_API_KEY environment variable is required. Generate one at ${KEY_HELP_URL} ` +\n `and add it to your MCP client config under env.DATADIVE_API_KEY.`,\n );\n }\n\n const rawBase = (env.DATADIVE_API_BASE_URL ?? DEFAULT_BASE_URL).trim();\n // Normalize: strip trailing slashes so we can append \"/v1/...\" paths cleanly.\n const baseUrl = rawBase.replace(/\\/+$/, \"\");\n\n return { apiKey, baseUrl };\n}\n","/**\n * Builds the MCP server: registers each tool from the registry with the SDK.\n *\n * Error handling: if a tool handler throws (most commonly an ApiError from the\n * HTTP client), we surface the message back through MCP as a tool-level error\n * (`isError: true`). The LLM sees the message and can react — e.g. tell the\n * user to regenerate their API key on a 401.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { Config } from \"./config.js\";\nimport { allTools } from \"./tools/index.js\";\nimport { ApiError } from \"./http/errors.js\";\nimport { PKG_VERSION, takeUpgradeNotice } from \"./http/client.js\";\n\nexport function buildServer(config: Config): McpServer {\n const server = new McpServer({\n name: \"datadive\",\n version: PKG_VERSION,\n });\n\n for (const tool of allTools) {\n server.registerTool(\n tool.name,\n {\n title: tool.title,\n description: tool.description,\n inputSchema: tool.inputSchema,\n },\n async (args: Record<string, unknown>) => {\n try {\n const data = await tool.handler(args, { config });\n const content = [\n {\n type: \"text\" as const,\n text: JSON.stringify(data, null, 2),\n },\n ];\n // If the backend signaled a newer release, append a one-time upgrade\n // nudge so the model relays it to the user. takeUpgradeNotice() returns\n // null after the first time, so this fires at most once per session.\n const notice = takeUpgradeNotice();\n if (notice) {\n content.push({\n type: \"text\" as const,\n text: `⚠️ datadive-mcp update available — ${notice}`,\n });\n }\n return { content };\n } catch (err) {\n return {\n isError: true,\n content: [\n {\n type: \"text\" as const,\n text: err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err),\n },\n ],\n };\n }\n },\n );\n }\n\n return server;\n}\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { NicheList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n currentPage: z\n .number()\n .int()\n .min(1)\n .optional()\n .describe(\"Page number, 1-indexed. Defaults to 1.\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Items per page (max 100). Defaults to 20.\"),\n};\n\nexport const listNichesTool: ToolDefinition<typeof inputSchema> = {\n name: \"list_niches\",\n title: \"List DataDive Niches\",\n description:\n \"Use this first when the user asks about their niches, or to find a `nicheId` for use with \" +\n \"`get_niche_keywords`, `get_niche_competitors`, or `get_ranking_juice`. \" +\n \"Retrieves a paginated list of Niches. Each Niche represents a market segment or product \" +\n \"category being tracked. Returns nicheId, heroKeyword, nicheLabel, marketplace (com/uk/de/...), \" +\n \"and latestResearchDate per niche, plus pagination metadata (currentPage, pageSize, total, \" +\n \"lastPage, hasNext, hasPrev).\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<NicheList>(\n { config: ctx.config, toolName: \"list_niches\" },\n \"/v1/niches\",\n { currentPage: args.currentPage, pageSize: args.pageSize },\n );\n },\n};\n","/**\n * Error mapping for the DataDive /v1 API.\n *\n * The MCP server is a thin HTTP shim — when api.datadive.tools returns an error,\n * we want the LLM (and ultimately the user) to see a useful, actionable message\n * rather than \"Request failed\". `ApiError.fromHttp` turns an HTTP status + body\n * into one of those messages.\n *\n * Source of truth for status semantics:\n * datadive-backend/src/external-api/auth/api-key.guard.ts (401/402 logic)\n * datadive-backend/src/external-api/external-api-v1.controller.ts (per-route docs)\n */\n\nconst KEY_HELP_URL = \"https://2.datadive.tools/api-key\";\n\nexport type ApiErrorKind =\n | \"auth\"\n | \"payment\"\n | \"forbidden\"\n | \"not_found\"\n | \"bad_request\"\n | \"rate_limit\"\n | \"server\"\n | \"network\"\n | \"http\";\n\nexport class ApiError extends Error {\n public readonly kind: ApiErrorKind;\n public readonly status: number;\n public readonly body: unknown;\n\n constructor(kind: ApiErrorKind, status: number, message: string, body?: unknown) {\n super(message);\n this.name = \"ApiError\";\n this.kind = kind;\n this.status = status;\n this.body = body;\n }\n\n static fromHttp(status: number, body: unknown): ApiError {\n const serverMsg = extractMessage(body);\n\n if (status === 400) {\n return new ApiError(\n \"bad_request\",\n 400,\n serverMsg ? `Bad request: ${serverMsg}` : \"Bad request: invalid parameters.\",\n body,\n );\n }\n if (status === 401) {\n return new ApiError(\n \"auth\",\n 401,\n `Authentication failed: your DATADIVE_API_KEY is invalid or expired. Generate a new key at ${KEY_HELP_URL}.`,\n body,\n );\n }\n if (status === 402) {\n return new ApiError(\n \"payment\",\n 402,\n \"Subscription is inactive or paused. Resume billing at https://2.datadive.tools to use the API.\",\n body,\n );\n }\n if (status === 403) {\n return new ApiError(\n \"forbidden\",\n 403,\n serverMsg\n ? `Forbidden: ${serverMsg}`\n : \"Forbidden: this API key cannot access the requested resource.\",\n body,\n );\n }\n if (status === 404) {\n return new ApiError(\"not_found\", 404, serverMsg ? `Not found: ${serverMsg}` : \"Resource not found.\", body);\n }\n if (status === 429) {\n return new ApiError(\"rate_limit\", 429, \"Rate limit exceeded. Wait a few seconds and retry.\", body);\n }\n if (status >= 500) {\n return new ApiError(\n \"server\",\n status,\n `DataDive API error (${status}). ${serverMsg ?? \"Please try again.\"}`.trim(),\n body,\n );\n }\n return new ApiError(\"http\", status, serverMsg ?? `HTTP ${status}`, body);\n }\n}\n\nfunction extractMessage(body: unknown): string | null {\n if (!body || typeof body !== \"object\") return null;\n const b = body as Record<string, unknown>;\n // NestJS conventional error shapes: { message: string | string[] } or { error, message }.\n if (typeof b.message === \"string\") return b.message;\n if (Array.isArray(b.message) && b.message.every((m) => typeof m === \"string\")) {\n return (b.message as string[]).join(\"; \");\n }\n if (typeof b.error === \"string\") return b.error;\n return null;\n}\n","{\n \"name\": \"@datadive-tools/mcp\",\n \"version\": \"0.5.0\",\n \"description\": \"MCP server for DataDive — query your niches, keywords, competitors, and Rank Radar data from Claude Desktop, Claude Code, or Cursor.\",\n \"type\": \"module\",\n \"bin\": {\n \"datadive-mcp\": \"./dist/index.js\"\n },\n \"main\": \"./dist/index.js\",\n \"files\": [\n \"dist\",\n \"README.md\",\n \"LICENSE\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"dev\": \"tsup --watch\",\n \"test\": \"vitest run\",\n \"test:coverage\": \"vitest run --coverage --coverage.include=\\\"src/**\\\" --coverage.reporter=json-summary --coverage.reporter=text\",\n \"test:watch\": \"vitest\",\n \"test:smoke\": \"DATADIVE_SMOKE=1 vitest run test/smoke.test.ts\",\n \"lint\": \"eslint .\",\n \"typecheck\": \"tsc --noEmit\",\n \"prepublishOnly\": \"npm run lint && npm run typecheck && npm run test && npm run build\"\n },\n \"keywords\": [\n \"mcp\",\n \"model-context-protocol\",\n \"datadive\",\n \"amazon\",\n \"claude\"\n ],\n \"author\": \"DataDive\",\n \"license\": \"MIT\",\n \"homepage\": \"https://github.com/Data-Dive-Tools/datadive-mcp\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/Data-Dive-Tools/datadive-mcp.git\"\n },\n \"bugs\": {\n \"url\": \"https://github.com/Data-Dive-Tools/datadive-mcp/issues\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.0.0\",\n \"zod\": \"^3.23.0\"\n },\n \"devDependencies\": {\n \"@changesets/cli\": \"^2.27.0\",\n \"@types/node\": \"^20.0.0\",\n \"@typescript-eslint/eslint-plugin\": \"^8.0.0\",\n \"@typescript-eslint/parser\": \"^8.0.0\",\n \"@vitest/coverage-v8\": \"4.1.8\",\n \"eslint\": \"^9.0.0\",\n \"prettier\": \"^3.0.0\",\n \"tsup\": \"^8.0.0\",\n \"typescript\": \"~5.9.0\",\n \"vitest\": \"4.1.8\"\n }\n}\n","/**\n * HTTP client for the DataDive /v1 API.\n *\n * Responsibilities:\n * 1. Attach `x-api-key` from config to every request.\n * 2. Tag the User-Agent with the package version + tool name so backend access\n * logs can attribute usage per tool (no separate telemetry endpoint needed).\n * 3. Map HTTP errors via `ApiError.fromHttp`.\n * 4. Unwrap the `ResponseDto<T>` envelope (`{ message?, success?, data }`)\n * while preserving bare `PaginationResponse<T>` bodies (which need their\n * pagination metadata to reach the LLM).\n *\n * Envelope detection (verified against datadive-backend):\n * - GET /v1/niches -> NicheList extends PaginationResponse<NicheItem> (BARE)\n * - GET /v1/niches/rank-radars -> ResponseDto<ExternalRankRadarListResponseDto>\n * where the inner DTO IS itself paginated.\n * - All other read endpoints -> ResponseDto<T> (UNWRAP)\n *\n * Heuristic: if the body has `data` AND any pagination key (currentPage, lastPage,\n * hasNext, hasPrev, pageSize, total), it's a PaginationResponse — return as-is.\n * Otherwise if the body has `data`, it's a ResponseDto envelope — return body.data.\n *\n * Cite: datadive-backend/src/common/pagination/pagination.dto.ts (pagination keys)\n * datadive-backend/src/common/dto/response.dto.ts (envelope shape)\n */\n\nimport type { Config } from \"../config.js\";\nimport { ApiError } from \"./errors.js\";\nimport { version } from \"../../package.json\";\n\n/**\n * The running package version, inlined from package.json at build time by\n * tsup/esbuild (and by vite under vitest). Single source of truth — no manual\n * bump here, so it can never drift from what's published to npm.\n *\n * Written into the User-Agent header, and compared against the backend's\n * advertised latest version (see noteLatestVersion) to nudge stale installs.\n */\nexport const PKG_VERSION: string = version;\n\n/** Response header the DataDive backend uses to advertise the latest published MCP version. */\nconst LATEST_VERSION_HEADER = \"x-datadive-mcp-latest\";\n\n/**\n * One-time upgrade notice. Armed when the backend advertises a version newer than\n * the one we're running; consumed (and then suppressed for the rest of the session)\n * by takeUpgradeNotice(). We surface it once per process so we don't nag on every call.\n */\nlet pendingUpgradeNotice: string | null = null;\nlet hasWarned = false;\n\n/**\n * Compares dotted version strings (major.minor.patch). Pre-release / build suffixes\n * are stripped — a stable release is the signal we act on. Returns true iff `latest`\n * is strictly newer than `current`. Exported for unit testing.\n */\nexport function isNewerVersion(latest: string, current: string): boolean {\n const parse = (v: string) => v.split(/[-+]/)[0]!.split(\".\").map((n) => parseInt(n, 10) || 0);\n const a = parse(latest);\n const b = parse(current);\n for (let i = 0; i < 3; i++) {\n const da = a[i] ?? 0;\n const db = b[i] ?? 0;\n if (da !== db) return da > db;\n }\n return false;\n}\n\n/** Records the advertised latest version; arms a one-time notice if we're behind. Never throws. */\nfunction noteLatestVersion(latest: string | null): void {\n if (!latest || hasWarned) return;\n if (isNewerVersion(latest, PKG_VERSION)) {\n pendingUpgradeNotice =\n `A newer version of datadive-mcp is available (${latest}; you are running ${PKG_VERSION}). ` +\n `Update by reinstalling the latest from npm — e.g. \\`npm install -g @datadive-tools/mcp@latest\\` — ` +\n `or bump the @datadive-tools/mcp version in your MCP client config, then restart the client.`;\n }\n}\n\n/**\n * Returns a pending upgrade notice once, then suppresses further notices for the rest\n * of the session. Called when assembling a tool result so the LLM can relay it to the\n * user. Returns null when there's nothing to surface.\n */\nexport function takeUpgradeNotice(): string | null {\n if (!pendingUpgradeNotice) return null;\n const notice = pendingUpgradeNotice;\n pendingUpgradeNotice = null;\n hasWarned = true;\n return notice;\n}\n\nconst PAGINATION_KEYS = [\"currentPage\", \"lastPage\", \"hasNext\", \"hasPrev\", \"pageSize\", \"total\"] as const;\n\nexport interface RequestContext {\n config: Config;\n /** Tool name used to tag the User-Agent header for per-tool log attribution. */\n toolName: string;\n}\n\nexport async function httpGet<T>(ctx: RequestContext, path: string, query?: Record<string, unknown>): Promise<T> {\n const url = buildUrl(ctx.config.baseUrl, path, query);\n\n let res: Response;\n try {\n res = await fetch(url, {\n method: \"GET\",\n headers: {\n \"x-api-key\": ctx.config.apiKey,\n accept: \"application/json\",\n \"user-agent\": userAgent(ctx.toolName),\n },\n });\n } catch (e) {\n const msg = e instanceof Error ? e.message : String(e);\n throw new ApiError(\"network\", 0, `Network error reaching ${url}: ${msg}`);\n }\n\n // The backend advertises the latest published MCP version on every response\n // (including errors); arm a one-time upgrade notice if we're behind.\n noteLatestVersion(res.headers.get(LATEST_VERSION_HEADER));\n\n const text = await res.text();\n let body: unknown;\n try {\n body = text.length > 0 ? JSON.parse(text) : null;\n } catch {\n body = text;\n }\n\n if (!res.ok) {\n throw ApiError.fromHttp(res.status, body);\n }\n\n return unwrap<T>(body);\n}\n\nfunction buildUrl(baseUrl: string, path: string, query?: Record<string, unknown>): string {\n const normalizedPath = path.startsWith(\"/\") ? path : `/${path}`;\n const u = new URL(`${baseUrl}${normalizedPath}`);\n if (query) {\n for (const [k, v] of Object.entries(query)) {\n if (v === undefined || v === null) continue;\n u.searchParams.set(k, String(v));\n }\n }\n return u.toString();\n}\n\nfunction userAgent(toolName: string): string {\n return `datadive-mcp/${PKG_VERSION} tool/${toolName}`;\n}\n\n/**\n * Unwraps a ResponseDto<T> envelope while preserving bare PaginationResponse bodies.\n * Exported for unit testing.\n */\nexport function unwrap<T>(body: unknown): T {\n if (!isObject(body)) return body as T;\n if (!(\"data\" in body)) return body as T;\n // If pagination metadata is present at the top level, return as-is.\n for (const key of PAGINATION_KEYS) {\n if (key in body) return body as T;\n }\n // Plain ResponseDto envelope -> unwrap.\n return (body as { data: T }).data;\n}\n\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { GetMasterKeywordListResult } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getNicheKeywordsTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_niche_keywords\",\n title: \"Get Master Keyword List for a Niche\",\n description:\n \"Use this when the user asks about keywords, search terms, or search volume for a niche. \" +\n \"Retrieves the master keyword list for the specified Niche — the relevant search terms \" +\n \"monitored for ranking and performance metrics. For each keyword, returns search volume, \" +\n \"relevancy (numeric score or \\\"Outlier\\\"), and competitor ASIN organic ranks \" +\n \"(asinRanks: { ASIN -> rank | null }). Also returns the latestResearchDate.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<GetMasterKeywordListResult>(\n { config: ctx.config, toolName: \"get_niche_keywords\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { NicheRoots } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getNicheRootsTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_niche_roots\",\n title: \"Get Keyword Roots for a Niche\",\n description:\n \"Use this when the user wants to find the highest-impact words across a niche's keywords, asks about \" +\n \"'keyword roots', or wants to prioritize terms for a listing. \" +\n \"Retrieves the keyword lexical roots for the Niche — individual words or word-combinations extracted \" +\n \"from the Master Keyword List (e.g. 'bluetooth headphones' yields roots 'bluetooth', 'headphones', \" +\n \"'bluetooth headphones'). Returns `roots` and `normalizedRoots` tables, each item with root, frequency \" +\n \"(how many keywords contain it), broadSearchVolume (summed search volume of those keywords), and \" +\n \"broadSearchVolumeRatio (0-1, relative to the top root), plus the per-keyword breakdowns and \" +\n \"latestResearchDate.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<NicheRoots>(\n { config: ctx.config, toolName: \"get_niche_roots\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/roots`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { GetCompetitorListResult } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getNicheCompetitorsTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_niche_competitors\",\n title: \"Get Competitors and Niche Statistics\",\n description:\n \"Use this for competitor ASINs, sales/revenue benchmarks, or niche opportunity scoring. \" +\n \"Retrieves the list of Competitors within the specified Niche along with Niche statistics. \" +\n \"For the Niche: keyword statistics, opportunity evaluation, benchmark median values, and \" +\n \"overall competitor strength assessment. For each Competitor: ASIN, sales, revenue, ratings, \" +\n \"reviews, price, image URL, and ranking data.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<GetCompetitorListResult>(\n { config: ctx.config, toolName: \"get_niche_competitors\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/competitors`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { NicheRankingJuices } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n nicheId: z.string().min(1).describe(\"The unique identifier of the Niche (from `list_niches`).\"),\n};\n\nexport const getRankingJuiceTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_ranking_juice\",\n title: \"Get Ranking Juices for a Niche\",\n description:\n \"Use this when the user asks 'how do I rank higher', 'what's my ranking juice', or wants \" +\n \"listing-optimization guidance. \" +\n \"Retrieves the Ranking Juices for each Competitor within the specified Niche. Ranking Juice is \" +\n \"DataDive's proprietary metric identifying the key factors driving search-ranking success \" +\n \"for each Competitor's listing. Returns currentListing and optimizedListing breakdowns \" +\n \"(rankingJuice + per-property contributions: title, bullets, description) plus per-competitor \" +\n \"listing scores and the latestResearchDate.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<NicheRankingJuices>(\n { config: ctx.config, toolName: \"get_ranking_juice\" },\n `/v1/niches/${encodeURIComponent(args.nicheId)}/ranking-juices`,\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport { RANK_RADAR_STATUSES, type RankRadarList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n currentPage: z.number().int().min(1).optional().describe(\"Page number, 1-indexed. Defaults to 1.\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe(\"Items per page (max 100). Defaults to 20.\"),\n nicheId: z\n .string()\n .min(1)\n .optional()\n .describe(\"Filter Rank Radars by Niche identifier. Use after `list_niches`.\"),\n status: z\n .enum(RANK_RADAR_STATUSES)\n .optional()\n .describe(\n \"Filter by Rank Radar status. By default, returns only ACTIVE Rank Radars. Allowed: ACTIVE, PAUSED, ARCHIVED.\",\n ),\n searchText: z.string().optional().describe(\"Filter Rank Radars by ASIN or product title.\"),\n};\n\nexport const listRankRadarsTool: ToolDefinition<typeof inputSchema> = {\n name: \"list_rank_radars\",\n title: \"List Rank Radars\",\n description:\n \"Use this to find a `rankRadarId` before calling `get_rank_radar_data`. Filter by `nicheId` \" +\n \"if the user has already identified a niche. \" +\n \"Retrieves a paginated list of Rank Radars — keyword-rank trackers monitoring organic and \" +\n \"sponsored positions for specific ASINs over time. Each item includes id, asin (with id, krtId, \" +\n \"asin), marketplace, keywordCount, title, imageUrl, and summary metrics: top10KW, top10SV, \" +\n \"top50KW, top50SV. Supports pagination metadata (currentPage, pageSize, total, lastPage, \" +\n \"hasNext, hasPrev).\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<RankRadarList>(\n { config: ctx.config, toolName: \"list_rank_radars\" },\n \"/v1/niches/rank-radars\",\n {\n currentPage: args.currentPage,\n pageSize: args.pageSize,\n nicheId: args.nicheId,\n status: args.status,\n searchText: args.searchText,\n },\n );\n },\n};\n","/**\n * Hand-mirrored DTOs from the DataDive backend (v1 external API).\n *\n * Source of truth (in this priority order):\n * 1. Live Swagger: https://developer.datadive.tools/docs#/v1\n * (machine-readable: https://developer.datadive.tools/docs-json)\n * 2. Backend DTOs: datadive-backend/src/external-api/dto/*.ts\n * datadive-backend/src/niche-research/dto/rank-radar.response.dto.ts\n * datadive-backend/src/common/dto/response.dto.ts\n * datadive-backend/src/common/pagination/pagination.dto.ts\n * 3. Controller: datadive-backend/src/external-api/external-api-v1.controller.ts\n *\n * Last synced: 2026-06-17.\n *\n * The MCP server forwards JSON straight to the LLM, so deeply-nested DTOs are\n * intentionally typed loosely (with `unknown` or pass-through Records) where\n * the TS surface doesn't add safety. Pin tighter as use cases require.\n */\n\n// ─── Envelope + pagination ───────────────────────────────────────────────────\n\n/** Generic NestJS response envelope: { message?, success?, data? }. */\nexport interface ResponseEnvelope<T> {\n message?: string;\n /** @deprecated — use HTTP status as the success indicator. */\n success?: boolean;\n data?: T | null;\n}\n\n/** Standard pagination envelope returned by list endpoints. */\nexport interface PaginationResponse<T> {\n data: T[];\n currentPage: number;\n pageSize: number;\n total: number;\n lastPage: number;\n hasNext: boolean;\n hasPrev: boolean;\n}\n\n/**\n * ISO-8601 date or timestamp, mirroring the backend's `@IsISO8601()` (date-only\n * allowed). Shared by the date/timestamp query params across /v1 tools\n * (alerts `updatedSince`, usage `startDate`/`endDate`).\n */\nexport const ISO_8601 = /^\\d{4}-\\d{2}-\\d{2}(?:T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d+)?)?(?:Z|[+-]\\d{2}:?\\d{2})?)?$/;\n\n/**\n * Marketplace codes accepted by `marketplace` params across /v1 endpoints\n * (alerts, inventory). Mirrors the backend's `SupportedMarketplaceEnum`.\n */\nexport const SUPPORTED_MARKETPLACES = [\n \"com\",\n \"ca\",\n \"co.uk\",\n \"com.mx\",\n \"in\",\n \"fr\",\n \"de\",\n \"es\",\n \"it\",\n \"co.jp\",\n] as const;\n\n// ─── /v1/niches (NicheList = PaginationResponse<NicheItem>) ─────────────────\n\nexport interface NicheItem {\n nicheId: string;\n heroKeyword: string;\n nicheLabel: string;\n /** Amazon marketplace TLD: \"com\", \"uk\", \"de\", etc. */\n marketplace: string;\n /** ISO date string when the Niche was last researched. Nullable. */\n latestResearchDate: string | null;\n}\n\nexport type NicheList = PaginationResponse<NicheItem>;\n\n// ─── /v1/niches/:nicheId/keywords (GetMasterKeywordListResult, wrapped) ─────\n\n/** Map of competitor ASIN -> rank (or null when unranked). */\nexport type AsinRanks = Record<string, number | null>;\n\nexport interface MasterKeyword {\n keyword: string;\n searchVolume: number;\n /** \"Outlier\" or a numeric relevancy score. */\n relevancy: string | number;\n asinRanks: AsinRanks;\n}\n\nexport interface GetMasterKeywordListResult {\n keywords: MasterKeyword[];\n /** ISO date string. */\n latestResearchDate: string;\n}\n\n// ─── /v1/niches/:nicheId/competitors (GetCompetitorListResult, wrapped) ─────\n\n/**\n * Niche competitors response — many nested DTOs from the backend (MklStatisticsData,\n * OpportunityEvaluation, MklBenchmark, CompetitorsStrength, AsinListItem). We type\n * the outer shape and leave the inner objects as opaque records; the LLM sees the\n * full JSON and can reason about it. Tighten if a specific consumer needs it.\n */\nexport interface GetCompetitorListResult {\n /** Marketplace enum (AmazonStore in backend). */\n marketplace: string;\n statistics: Record<string, unknown>;\n opportunityEvaluation: Record<string, unknown>;\n benchmark: Record<string, unknown>;\n competitorsStrength: Record<string, unknown>;\n competitors: Array<Record<string, unknown>>;\n latestResearchDate: string;\n}\n\n// ─── /v1/niches/:nicheId/ranking-juices (NicheRankingJuices, wrapped) ───────\n\nexport interface RankingJuiceProperty {\n rankingJuice: number;\n}\n\nexport interface ListingRankingJuice {\n rankingJuice: number;\n title: RankingJuiceProperty;\n bullets: RankingJuiceProperty;\n description: RankingJuiceProperty;\n}\n\nexport interface CompetitorRankingJuice {\n asin: string;\n listing: ListingRankingJuice;\n}\n\nexport interface NicheRankingJuices {\n currentListing: ListingRankingJuice;\n optimizedListing: ListingRankingJuice;\n competitors: CompetitorRankingJuice[];\n latestResearchDate: string;\n}\n\n// ─── /v1/niches/rank-radars (ExternalRankRadarListResponseDto, wrapped) ─────\n\nexport interface RankRadarAsin {\n id: string;\n krtId: string;\n asin: string;\n}\n\nexport interface RankRadarItem {\n id: string;\n asin: RankRadarAsin;\n marketplace: string;\n keywordCount: number;\n title: string;\n imageUrl: string;\n top10KW: number | null;\n top10SV: number | null;\n top50KW: number | null;\n top50SV: number | null;\n}\n\nexport type RankRadarList = PaginationResponse<RankRadarItem>;\n\n/** Allowed values for the `status` query param on /v1/niches/rank-radars. */\nexport const RANK_RADAR_STATUSES = [\"ACTIVE\", \"PAUSED\", \"ARCHIVED\"] as const;\nexport type RankRadarStatus = (typeof RANK_RADAR_STATUSES)[number];\n\n// ─── /v1/niches/rank-radars/:rankRadarId (KrtKeywordResponseDto[], wrapped) ─\n\nexport interface KrtAsinRank {\n /** ISO date string. */\n date: string;\n organicRank?: number | null;\n impressionRank?: number | null;\n}\n\nexport interface KrtKeyword {\n id: string;\n keyword: string;\n searchVolume: number | null;\n ranks: KrtAsinRank[];\n /** Highlight annotations (KrtHighlightDto in backend); kept opaque for MVP. */\n highlights: Array<Record<string, unknown>>;\n}\n\nexport type RankRadarKeywordList = KrtKeyword[];\n\n// ─── /v1/sellers/:sellerId/marketplaces/:marketplace/asins/:asin/inventory ────\n// (InventoryByFcResponseDto, wrapped)\n\nexport interface InventoryByFcItem {\n /** Fulfillment center code. */\n fc: string;\n /** State or region code where the FC is located, when known. */\n state: string;\n /** Units of sellable inventory currently at this FC. May be 0 when stock is out. */\n availableStock: number;\n /** Share of the ASIN's total sellable inventory at this FC, as a 0..1 fraction. */\n availableStockPercentage: number;\n}\n\nexport interface InventoryByFcResponse {\n asin: string;\n sellerId: string;\n /** Marketplace enum: \"com\", \"ca\", \"co.uk\", \"com.mx\", \"in\", \"fr\", \"de\", \"es\", \"it\", \"co.jp\". */\n marketplace: string;\n /** ISO timestamp of last successful ingestion. Null if none in the last 30 days. */\n lastUpdatedAt: string | null;\n totalSellableUnits: number;\n distribution: InventoryByFcItem[];\n}\n\n// ─── /v1/alerts/indexing-issues (IndexingIssueAlertListDto, bare) ───────────\n\n/**\n * Allowed values for the `status` query param on the /v1/alerts/* endpoints.\n * `active` is the server-side default; dismissed alerts are never returned.\n */\nexport const ALERT_STATUSES = [\"active\", \"resolved\", \"all\"] as const;\nexport type AlertStatus = (typeof ALERT_STATUSES)[number];\n\nexport interface IndexingIssueAlertItem {\n id: number;\n /** The ASIN that is no longer indexed for its tracked keywords. */\n asin: string;\n title: string | null;\n imageUrl: string | null;\n /** True when the ASIN is a parent of a variation family. */\n isParent: boolean;\n sellerId: string;\n marketplace: string;\n /** ISO timestamp of when the alert was most recently surfaced. Updates each time it re-fires. */\n lastAlertedAt: string;\n /** ISO timestamp of when the alert was resolved, or null if still active. */\n resolvedAt: string | null;\n}\n\nexport type IndexingIssueAlertList = PaginationResponse<IndexingIssueAlertItem>;\n\n// ─── /v1/alerts/blind-spend (BlindSpendAlertListDto, bare) ──────────────────\n\nexport interface BlindSpendSearchTerm {\n /** The customer search term that wasted spend. */\n term: string;\n spend: number | null;\n sales: number | null;\n clicks: number | null;\n /** Conversion rate as a 0..1 fraction. */\n cvr: number | null;\n impressions: number | null;\n}\n\nexport interface BlindSpendAlertItem {\n id: number;\n asin: string | null;\n title: string | null;\n imageUrl: string | null;\n sellerId: string;\n marketplace: string;\n /** ISO timestamp of when the alert was most recently surfaced. Updates each time it re-fires. */\n lastAlertedAt: string;\n /** ISO timestamp of when the alert was resolved, or null if still active. */\n resolvedAt: string | null;\n /** Total ad spend across the unresolved wasted-spend search terms. */\n wastedSpend: number;\n totalKeywordCount: number;\n unresolvedKeywordCount: number;\n /** The unresolved wasted-spend search terms. */\n searchTerms: BlindSpendSearchTerm[];\n}\n\nexport type BlindSpendAlertList = PaginationResponse<BlindSpendAlertItem>;\n\n// ─── Billable features (shared by /v1/quota and /v1/usage) ───────────────────\n\n/**\n * Billable feature types. These are both the keys of the `quota.features` object\n * and the allowed values of the `type` filter on /v1/usage. Mirrors the backend's\n * billable-feature enum.\n */\nexport const BILLABLE_FEATURE_TYPES = [\n \"DIVED_ASINS\",\n \"PRODUCT_BRIEF_ASINS\",\n \"AI_COPYWRITER_PROMPTS\",\n \"RANK_RADAR_KEYWORDS\",\n] as const;\nexport type BillableFeatureType = (typeof BILLABLE_FEATURE_TYPES)[number];\n\n// ─── /v1/niches/:nicheId/roots (ExternalRootResponseDto[], wrapped) ─────────\n\nexport interface RootsTableItem {\n /** A word or word-combination extracted from the master keyword list. */\n root: string;\n /** Count of keywords in the master list that contain this root. */\n frequency: number;\n /** Sum of search volume across all original keywords containing this root. */\n broadSearchVolume: number;\n /** broadSearchVolume / maxBroadSearchVolume, as a 0..1 fraction. */\n broadSearchVolumeRatio: number;\n}\n\n/**\n * Keyword-roots analysis for a Niche. `keywords` and `consolidatedKeywords` are\n * the per-keyword breakdowns (kept opaque — the LLM sees the full JSON); `roots`\n * and `normalizedRoots` are the ranked root tables.\n *\n * Note: the OpenAPI spec types the endpoint's `data` as an array, but the live\n * API returns a single object (verified against api-qa, 2026-06-17). The client's\n * unwrap() strips the `{ data }` envelope, so the tool returns this object directly.\n */\nexport interface NicheRoots {\n keywords: Array<Record<string, unknown>>;\n consolidatedKeywords: Array<Record<string, unknown>>;\n roots: RootsTableItem[];\n normalizedRoots: RootsTableItem[];\n /** ISO date string of the last successful research for the niche. */\n latestResearchDate: string;\n}\n\n// ─── /v1/quota (ExternalQuotaResponseDto) ───────────────────────────────────\n\nexport interface QuotaFeature {\n /** Current usage count. Null when not applicable to the plan. */\n used: number | null;\n /** Quota capacity. Null when unlimited / not applicable. */\n capacity: number | null;\n}\n\nexport interface Quota {\n /** ISO-8601 timestamp of the next quota reset. Null when no reset is scheduled. */\n nextRefreshDate: string | null;\n /** Per-billable-feature usage and capacity. */\n features: Record<BillableFeatureType, QuotaFeature>;\n}\n\n// ─── /v1/usage (ExternalUsageLogListDto, bare PaginationResponse) ───────────\n\nexport interface UsageLogItem {\n /** Name of the user who performed the action. Null when unknown. */\n name: string | null;\n email: string;\n /** Number of tokens consumed. */\n qty: number;\n type: BillableFeatureType;\n /** Specific action performed (e.g. \"RANK_RADAR_CREATE\"). Null when not set. */\n action: string | null;\n nicheId: string | null;\n nicheName: string | null;\n rankRadarId: string | null;\n /** ISO timestamp of when the usage was recorded. */\n date: string;\n}\n\nexport type UsageLogList = PaginationResponse<UsageLogItem>;\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport type { RankRadarKeywordList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\n/** yyyy-mm-dd matcher used by both startDate and endDate. */\nconst ISO_DATE = /^\\d{4}-\\d{2}-\\d{2}$/;\n\nconst inputSchema = {\n rankRadarId: z\n .string()\n .uuid()\n .describe(\"The Rank Radar UUID (from `list_rank_radars`).\"),\n startDate: z\n .string()\n .regex(ISO_DATE, \"startDate must be in yyyy-mm-dd format\")\n .describe(\"Start date for the ranking data range, yyyy-mm-dd (e.g. 2024-03-26).\"),\n endDate: z\n .string()\n .regex(ISO_DATE, \"endDate must be in yyyy-mm-dd format\")\n .describe(\"End date for the ranking data range, yyyy-mm-dd (e.g. 2024-04-26). Must be on or after startDate.\"),\n};\n\nexport const getRankRadarDataTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_rank_radar_data\",\n title: \"Get Keyword Rankings for a Rank Radar\",\n description:\n \"Use this to analyze keyword ranking trends over time. Requires startDate and endDate (yyyy-mm-dd). \" +\n \"Retrieves historical keyword ranking data for the specified Rank Radar within the date range. \" +\n \"Returns an array of tracked keywords; each has id, keyword, searchVolume, ranks (per-day \" +\n \"{ date, organicRank, impressionRank }), and any highlight annotations. Use after \" +\n \"`list_rank_radars` to discover a `rankRadarId`.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<RankRadarKeywordList>(\n { config: ctx.config, toolName: \"get_rank_radar_data\" },\n `/v1/niches/rank-radars/${encodeURIComponent(args.rankRadarId)}`,\n { startDate: args.startDate, endDate: args.endDate },\n );\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport { SUPPORTED_MARKETPLACES, type InventoryByFcResponse } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n sellerId: z\n .string()\n .min(1)\n .describe(\n \"Amazon seller account ID as connected to DataDive. Find it on the Connections page at https://2.datadive.tools — there is no MCP discovery tool for sellers yet.\",\n ),\n marketplace: z\n .enum(SUPPORTED_MARKETPLACES)\n .describe('Amazon marketplace code, e.g. \"com\" for amazon.com or \"co.uk\" for amazon.co.uk.'),\n asin: z\n .string()\n .regex(/^[A-Z0-9]{10}$/, \"ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).\")\n .describe(\n \"Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234).\",\n ),\n};\n\nexport const getAsinInventoryDistributionTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_asin_inventory_distribution\",\n title: \"Get Per-Fulfillment-Center Inventory Distribution for an ASIN\",\n description:\n \"Use this when the user asks about inventory levels, stock by fulfillment center, \" +\n \"or where their units are sitting across Amazon's fulfillment network for a specific ASIN. \" +\n \"Requires the Amazon sellerId from the user's DataDive Connections page \" +\n \"(https://2.datadive.tools), the marketplace code, and the ASIN. Returns totalSellableUnits \" +\n \"and a per-FC `distribution` array (fc, state, availableStock, availableStockPercentage). \" +\n \"`lastUpdatedAt` may be null when no successful inventory ingestion has occurred in the last 30 days.\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<InventoryByFcResponse>(\n { config: ctx.config, toolName: \"get_asin_inventory_distribution\" },\n `/v1/sellers/${encodeURIComponent(args.sellerId)}/marketplaces/${encodeURIComponent(\n args.marketplace,\n )}/asins/${encodeURIComponent(args.asin)}/inventory`,\n );\n },\n};\n","/**\n * Shared input schema for the /v1/alerts/* list tools. Both alert endpoints\n * accept the same backend query DTO (ExternalAlertsQueryDto), so the input\n * contract is defined once here and imported by each tool.\n */\n\nimport { z } from \"zod\";\nimport { ALERT_STATUSES, ISO_8601, SUPPORTED_MARKETPLACES } from \"../types/api.js\";\n\nexport const alertsQueryInputSchema = {\n sellerId: z\n .string()\n .min(1)\n .max(50)\n .optional()\n .describe('Filter to a single connected Amazon seller account ID, e.g. \"A1B2C3D4E5\".'),\n marketplace: z\n .enum(SUPPORTED_MARKETPLACES)\n .optional()\n .describe('Filter to a single Amazon marketplace code, e.g. \"com\" for amazon.com or \"co.uk\" for amazon.co.uk.'),\n status: z\n .enum(ALERT_STATUSES)\n .optional()\n .describe(\n \"Lifecycle filter. `active` (default) returns unresolved alerts, `resolved` returns resolved alerts, \" +\n \"`all` returns both. Dismissed alerts are never returned.\",\n ),\n updatedSince: z\n .string()\n .regex(ISO_8601, \"updatedSince must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)\")\n .optional()\n .describe(\n \"ISO-8601 timestamp; return only alerts surfaced at or after this time (filters on `lastAlertedAt`). \" +\n \"Use it to incrementally sync since your last poll. Defaults to the last 30 days when omitted.\",\n ),\n currentPage: z.number().int().min(1).optional().describe(\"Page number, 1-indexed. Defaults to 1.\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(50)\n .optional()\n .describe(\"Items per page (max 50). Defaults to 20.\"),\n};\n","import { httpGet } from \"../http/client.js\";\nimport type { IndexingIssueAlertList } from \"../types/api.js\";\nimport { alertsQueryInputSchema } from \"./alert-query.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nexport const listIndexingIssueAlertsTool: ToolDefinition<typeof alertsQueryInputSchema> = {\n name: \"list_indexing_issue_alerts\",\n title: \"List Indexing-Issue Alerts\",\n description:\n \"Use this to find products that may have lost search visibility. Retrieves a paginated list of \" +\n \"indexing-issue alerts across the user's connected Amazon seller accounts — an alert fires when one of \" +\n \"their ASINs is no longer indexed for its tracked keywords. By default returns active (unresolved) alerts \" +\n \"from the last 30 days; filter by `sellerId`, `marketplace`, `status`, or `updatedSince` (for incremental \" +\n \"polling). Each item includes id, asin, title, imageUrl, isParent, sellerId, marketplace, lastAlertedAt, \" +\n \"and resolvedAt. Supports pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).\",\n inputSchema: alertsQueryInputSchema,\n handler: async (args, ctx) => {\n return await httpGet<IndexingIssueAlertList>(\n { config: ctx.config, toolName: \"list_indexing_issue_alerts\" },\n \"/v1/alerts/indexing-issues\",\n {\n sellerId: args.sellerId,\n marketplace: args.marketplace,\n status: args.status,\n updatedSince: args.updatedSince,\n currentPage: args.currentPage,\n pageSize: args.pageSize,\n },\n );\n },\n};\n","import { httpGet } from \"../http/client.js\";\nimport type { BlindSpendAlertList } from \"../types/api.js\";\nimport { alertsQueryInputSchema } from \"./alert-query.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nexport const listBlindSpendAlertsTool: ToolDefinition<typeof alertsQueryInputSchema> = {\n name: \"list_blind_spend_alerts\",\n title: \"List Blind-Spend (Wasted-Spend) Alerts\",\n description:\n \"Use this to find wasted PPC ad spend. Retrieves a paginated list of blind-spend alerts across the \" +\n \"user's connected Amazon seller accounts — an alert flags ad spend on customer search terms that produced \" +\n \"little or no sales. By default returns active (unresolved) alerts from the last 30 days; filter by \" +\n \"`sellerId`, `marketplace`, `status`, or `updatedSince` (for incremental polling). Each item includes id, \" +\n \"asin, title, imageUrl, sellerId, marketplace, lastAlertedAt, resolvedAt, wastedSpend (total ad spend \" +\n \"across unresolved terms), totalKeywordCount, unresolvedKeywordCount, and searchTerms — the unresolved \" +\n \"wasted-spend search terms, each with term, spend, sales, clicks, cvr (0-1), and impressions. Supports \" +\n \"pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).\",\n inputSchema: alertsQueryInputSchema,\n handler: async (args, ctx) => {\n return await httpGet<BlindSpendAlertList>(\n { config: ctx.config, toolName: \"list_blind_spend_alerts\" },\n \"/v1/alerts/blind-spend\",\n {\n sellerId: args.sellerId,\n marketplace: args.marketplace,\n status: args.status,\n updatedSince: args.updatedSince,\n currentPage: args.currentPage,\n pageSize: args.pageSize,\n },\n );\n },\n};\n","import { httpGet } from \"../http/client.js\";\nimport type { Quota } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {};\n\nexport const getQuotaTool: ToolDefinition<typeof inputSchema> = {\n name: \"get_quota\",\n title: \"Get Quota Usage\",\n description:\n \"Use this when the user asks how much quota or how many tokens they have left, or before creating a \" +\n \"Niche Dive or Rank Radar to check remaining headroom. \" +\n \"Returns current quota usage and capacity for each billable feature — DIVED_ASINS (Dive tokens), \" +\n \"RANK_RADAR_KEYWORDS (tracked keywords), PRODUCT_BRIEF_ASINS, and AI_COPYWRITER_PROMPTS — each with \" +\n \"`used` and `capacity` (null when unlimited / not applicable), plus `nextRefreshDate` (ISO-8601 \" +\n \"timestamp of the next quota reset, or null). Takes no arguments.\",\n inputSchema,\n handler: async (_args, ctx) => {\n return await httpGet<Quota>({ config: ctx.config, toolName: \"get_quota\" }, \"/v1/quota\");\n },\n};\n","import { z } from \"zod\";\nimport { httpGet } from \"../http/client.js\";\nimport { BILLABLE_FEATURE_TYPES, ISO_8601, type UsageLogList } from \"../types/api.js\";\nimport type { ToolDefinition } from \"./types.js\";\n\nconst inputSchema = {\n type: z\n .enum(BILLABLE_FEATURE_TYPES)\n .optional()\n .describe(\"Filter to a single billable feature type. Omit to return all types.\"),\n search: z\n .string()\n .min(1)\n .optional()\n .describe(\"Case-insensitive partial match on the user's name or email.\"),\n startDate: z\n .string()\n .regex(ISO_8601, \"startDate must be an ISO-8601 date or timestamp (e.g. 2026-05-01 or 2026-05-01T00:00:00Z)\")\n .optional()\n .describe(\"ISO-8601 date/timestamp; return only usage logs recorded on or after this time.\"),\n endDate: z\n .string()\n .regex(ISO_8601, \"endDate must be an ISO-8601 date or timestamp (e.g. 2026-05-31 or 2026-05-31T23:59:59Z)\")\n .optional()\n .describe(\"ISO-8601 date/timestamp; return only usage logs recorded on or before this time.\"),\n currentPage: z.number().int().min(1).optional().describe(\"Page number, 1-indexed. Defaults to 1.\"),\n pageSize: z\n .number()\n .int()\n .min(1)\n .max(200)\n .optional()\n .describe(\"Items per page (max 200). Defaults to 50.\"),\n};\n\nexport const listUsageTool: ToolDefinition<typeof inputSchema> = {\n name: \"list_usage\",\n title: \"List Billable Usage Logs\",\n description:\n \"Use this when the user asks who consumed tokens, how their quota was spent, or wants an audit of \" +\n \"billable activity over a date range. \" +\n \"Retrieves a paginated list of billable feature usage logs for the organization — each entry is a \" +\n \"token-consumption event (a dive, rank-radar creation, AI copywriter prompt, etc.). Filter by `type` \" +\n \"(billable feature), `search` (user name/email), and `startDate`/`endDate`. Each item includes name, \" +\n \"email, qty (tokens consumed), type, action (specific operation), nicheId/nicheName, rankRadarId, and \" +\n \"date, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).\",\n inputSchema,\n handler: async (args, ctx) => {\n return await httpGet<UsageLogList>(\n { config: ctx.config, toolName: \"list_usage\" },\n \"/v1/usage\",\n {\n type: args.type,\n search: args.search,\n startDate: args.startDate,\n endDate: args.endDate,\n currentPage: args.currentPage,\n pageSize: args.pageSize,\n },\n );\n },\n};\n","/**\n * Tool registry. The order here is the order tools appear in `tools/list`\n * over the MCP transport, which biases LLM discovery — keep \"discovery\" tools\n * (the ones that return IDs needed by other tools) at the top.\n */\n\nimport { listNichesTool } from \"./list-niches.js\";\nimport { getNicheKeywordsTool } from \"./get-niche-keywords.js\";\nimport { getNicheRootsTool } from \"./get-niche-roots.js\";\nimport { getNicheCompetitorsTool } from \"./get-niche-competitors.js\";\nimport { getRankingJuiceTool } from \"./get-ranking-juice.js\";\nimport { listRankRadarsTool } from \"./list-rank-radars.js\";\nimport { getRankRadarDataTool } from \"./get-rank-radar-data.js\";\nimport { getAsinInventoryDistributionTool } from \"./get-asin-inventory-distribution.js\";\nimport { listIndexingIssueAlertsTool } from \"./list-indexing-issue-alerts.js\";\nimport { listBlindSpendAlertsTool } from \"./list-blind-spend-alerts.js\";\nimport { getQuotaTool } from \"./get-quota.js\";\nimport { listUsageTool } from \"./list-usage.js\";\nimport type { AnyTool } from \"./types.js\";\n\n// Note on the cast: each ToolDefinition<S> has a handler typed to its specific\n// schema; the registry needs a uniform shape. The MCP SDK validates incoming\n// args against `inputSchema` before calling the handler, so the cast is safe.\nexport const allTools: ReadonlyArray<AnyTool> = [\n listNichesTool,\n getNicheKeywordsTool,\n getNicheRootsTool,\n getNicheCompetitorsTool,\n getRankingJuiceTool,\n listRankRadarsTool,\n getRankRadarDataTool,\n getAsinInventoryDistributionTool,\n listIndexingIssueAlertsTool,\n listBlindSpendAlertsTool,\n getQuotaTool,\n listUsageTool,\n] as unknown as ReadonlyArray<AnyTool>;\n\nexport type { ToolDefinition, AnyTool } from \"./types.js\";\n"],"mappings":"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","names":["KEY_HELP_URL","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","inputSchema","z","z","z","inputSchema","z","inputSchema","z"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datadive-tools/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for DataDive — query your niches, keywords, competitors, and Rank Radar data from Claude Desktop, Claude Code, or Cursor.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "build": "tsup",
20
20
  "dev": "tsup --watch",
21
21
  "test": "vitest run",
22
+ "test:coverage": "vitest run --coverage --coverage.include=\"src/**\" --coverage.reporter=json-summary --coverage.reporter=text",
22
23
  "test:watch": "vitest",
23
24
  "test:smoke": "DATADIVE_SMOKE=1 vitest run test/smoke.test.ts",
24
25
  "lint": "eslint .",
@@ -51,10 +52,11 @@
51
52
  "@types/node": "^20.0.0",
52
53
  "@typescript-eslint/eslint-plugin": "^8.0.0",
53
54
  "@typescript-eslint/parser": "^8.0.0",
55
+ "@vitest/coverage-v8": "4.1.8",
54
56
  "eslint": "^9.0.0",
55
57
  "prettier": "^3.0.0",
56
58
  "tsup": "^8.0.0",
57
59
  "typescript": "~5.9.0",
58
- "vitest": "^2.0.0"
60
+ "vitest": "4.1.8"
59
61
  }
60
62
  }