@datadive-tools/mcp 0.7.0 → 0.9.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/dist/lib.js ADDED
@@ -0,0 +1,909 @@
1
+ // src/server.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+
4
+ // src/config.ts
5
+ var SCOPE_READ = "datadive.read";
6
+ var SCOPE_WRITE = "datadive.write";
7
+ var DEFAULT_BASE_URL = "https://api.datadive.tools";
8
+ var KEY_HELP_URL = "https://2.datadive.tools/api-key";
9
+ function loadConfig(env = process.env) {
10
+ const apiKey = (env.DATADIVE_API_KEY ?? "").trim();
11
+ if (!apiKey) {
12
+ throw new Error(
13
+ `DATADIVE_API_KEY environment variable is required. Generate one at ${KEY_HELP_URL} and add it to your MCP client config under env.DATADIVE_API_KEY.`
14
+ );
15
+ }
16
+ const rawBase = (env.DATADIVE_API_BASE_URL ?? DEFAULT_BASE_URL).trim();
17
+ const baseUrl = rawBase.replace(/\/+$/, "");
18
+ const autoConfirmWrites = ["1", "true", "yes"].includes(
19
+ (env.DATADIVE_AUTO_CONFIRM_WRITES ?? "").trim().toLowerCase()
20
+ );
21
+ return { credentials: { kind: "api-key", apiKey }, baseUrl, autoConfirmWrites };
22
+ }
23
+
24
+ // src/tools/list-niches.ts
25
+ import { z } from "zod";
26
+
27
+ // src/http/errors.ts
28
+ var KEY_HELP_URL2 = "https://2.datadive.tools/api-key";
29
+ var ApiError = class _ApiError extends Error {
30
+ kind;
31
+ status;
32
+ body;
33
+ constructor(kind, status, message, body) {
34
+ super(message);
35
+ this.name = "ApiError";
36
+ this.kind = kind;
37
+ this.status = status;
38
+ this.body = body;
39
+ }
40
+ static fromHttp(status, body) {
41
+ const serverMsg = extractMessage(body);
42
+ if (status === 400) {
43
+ return new _ApiError(
44
+ "bad_request",
45
+ 400,
46
+ serverMsg ? `Bad request: ${serverMsg}` : "Bad request: invalid parameters.",
47
+ body
48
+ );
49
+ }
50
+ if (status === 401) {
51
+ return new _ApiError(
52
+ "auth",
53
+ 401,
54
+ `Authentication failed: your DATADIVE_API_KEY is invalid or expired. Generate a new key at ${KEY_HELP_URL2}.`,
55
+ body
56
+ );
57
+ }
58
+ if (status === 402) {
59
+ return new _ApiError(
60
+ "payment",
61
+ 402,
62
+ "Subscription is inactive or paused. Resume billing at https://2.datadive.tools to use the API.",
63
+ body
64
+ );
65
+ }
66
+ if (status === 403) {
67
+ return new _ApiError(
68
+ "forbidden",
69
+ 403,
70
+ serverMsg ? `Forbidden: ${serverMsg}` : "Forbidden: this API key cannot access the requested resource.",
71
+ body
72
+ );
73
+ }
74
+ if (status === 404) {
75
+ return new _ApiError("not_found", 404, serverMsg ? `Not found: ${serverMsg}` : "Resource not found.", body);
76
+ }
77
+ if (status === 429) {
78
+ return new _ApiError("rate_limit", 429, "Rate limit exceeded. Wait a few seconds and retry.", body);
79
+ }
80
+ if (status >= 500) {
81
+ return new _ApiError(
82
+ "server",
83
+ status,
84
+ `DataDive API error (${status}). ${serverMsg ?? "Please try again."}`.trim(),
85
+ body
86
+ );
87
+ }
88
+ return new _ApiError("http", status, serverMsg ?? `HTTP ${status}`, body);
89
+ }
90
+ };
91
+ function extractMessage(body) {
92
+ if (!body || typeof body !== "object") return null;
93
+ const b = body;
94
+ if (typeof b.message === "string") return b.message;
95
+ if (Array.isArray(b.message) && b.message.every((m) => typeof m === "string")) {
96
+ return b.message.join("; ");
97
+ }
98
+ if (typeof b.error === "string") return b.error;
99
+ return null;
100
+ }
101
+
102
+ // package.json
103
+ var version = "0.9.0";
104
+
105
+ // src/http/client.ts
106
+ var PKG_VERSION = version;
107
+ var LATEST_VERSION_HEADER = "x-datadive-mcp-latest";
108
+ var pendingUpgradeNotice = null;
109
+ var hasWarned = false;
110
+ function isNewerVersion(latest, current) {
111
+ const parse = (v) => v.split(/[-+]/)[0].split(".").map((n) => parseInt(n, 10) || 0);
112
+ const a = parse(latest);
113
+ const b = parse(current);
114
+ for (let i = 0; i < 3; i++) {
115
+ const da = a[i] ?? 0;
116
+ const db = b[i] ?? 0;
117
+ if (da !== db) return da > db;
118
+ }
119
+ return false;
120
+ }
121
+ function noteLatestVersion(latest) {
122
+ if (!latest || hasWarned) return;
123
+ if (isNewerVersion(latest, PKG_VERSION)) {
124
+ pendingUpgradeNotice = `A newer version of datadive-mcp is available (${latest}; you are running ${PKG_VERSION}). Update by reinstalling the latest from npm \u2014 e.g. \`npm install -g @datadive-tools/mcp@latest\` \u2014 or bump the @datadive-tools/mcp version in your MCP client config, then restart the client.`;
125
+ }
126
+ }
127
+ function takeUpgradeNotice() {
128
+ if (!pendingUpgradeNotice) return null;
129
+ const notice = pendingUpgradeNotice;
130
+ pendingUpgradeNotice = null;
131
+ hasWarned = true;
132
+ return notice;
133
+ }
134
+ var PAGINATION_KEYS = ["currentPage", "lastPage", "hasNext", "hasPrev", "pageSize", "total"];
135
+ async function httpGet(ctx, path, query) {
136
+ const url = buildUrl(ctx.config.baseUrl, path, query);
137
+ let res;
138
+ try {
139
+ res = await fetch(url, {
140
+ method: "GET",
141
+ headers: {
142
+ ...authHeaders(ctx.config),
143
+ accept: "application/json",
144
+ "user-agent": userAgent(ctx.toolName)
145
+ }
146
+ });
147
+ } catch (e) {
148
+ throw networkError(url, e);
149
+ }
150
+ return handleResponse(res);
151
+ }
152
+ async function httpPost(ctx, path, body) {
153
+ const url = buildUrl(ctx.config.baseUrl, path);
154
+ let res;
155
+ try {
156
+ res = await fetch(url, {
157
+ method: "POST",
158
+ headers: {
159
+ ...authHeaders(ctx.config),
160
+ accept: "application/json",
161
+ "content-type": "application/json",
162
+ "user-agent": userAgent(ctx.toolName)
163
+ },
164
+ body: JSON.stringify(body)
165
+ });
166
+ } catch (e) {
167
+ throw networkError(url, e);
168
+ }
169
+ return handleResponse(res);
170
+ }
171
+ function networkError(url, e) {
172
+ const msg = e instanceof Error ? e.message : String(e);
173
+ return new ApiError("network", 0, `Network error reaching ${url}: ${msg}`);
174
+ }
175
+ async function handleResponse(res) {
176
+ noteLatestVersion(res.headers.get(LATEST_VERSION_HEADER));
177
+ const text = await res.text();
178
+ let body;
179
+ try {
180
+ body = text.length > 0 ? JSON.parse(text) : null;
181
+ } catch {
182
+ body = text;
183
+ }
184
+ if (!res.ok) {
185
+ throw ApiError.fromHttp(res.status, body);
186
+ }
187
+ return unwrap(body);
188
+ }
189
+ function buildUrl(baseUrl, path, query) {
190
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
191
+ const u = new URL(`${baseUrl}${normalizedPath}`);
192
+ if (query) {
193
+ for (const [k, v] of Object.entries(query)) {
194
+ if (v === void 0 || v === null) continue;
195
+ u.searchParams.set(k, String(v));
196
+ }
197
+ }
198
+ return u.toString();
199
+ }
200
+ function userAgent(toolName) {
201
+ return `datadive-mcp/${PKG_VERSION} tool/${toolName}`;
202
+ }
203
+ function authHeaders(config) {
204
+ const c = config.credentials;
205
+ return c.kind === "bearer" ? { authorization: `Bearer ${c.token}` } : { "x-api-key": c.apiKey };
206
+ }
207
+ function unwrap(body) {
208
+ if (!isObject(body)) return body;
209
+ if (!("data" in body)) return body;
210
+ for (const key of PAGINATION_KEYS) {
211
+ if (key in body) return body;
212
+ }
213
+ return body.data;
214
+ }
215
+ function isObject(v) {
216
+ return typeof v === "object" && v !== null && !Array.isArray(v);
217
+ }
218
+
219
+ // src/tools/list-niches.ts
220
+ var inputSchema = {
221
+ currentPage: z.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
222
+ pageSize: z.number().int().min(1).max(100).optional().describe("Items per page (max 100). Defaults to 20.")
223
+ };
224
+ var listNichesTool = {
225
+ name: "list_niches",
226
+ title: "List DataDive Niches",
227
+ description: "Use this first when the user asks about their niches, or to find a `nicheId` for use with `get_niche_keywords`, `get_niche_competitors`, or `get_ranking_juice`. Retrieves a paginated list of Niches. Each Niche represents a market segment or product category being tracked. Returns nicheId, heroKeyword, nicheLabel, marketplace (com/uk/de/...), and latestResearchDate per niche, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
228
+ inputSchema,
229
+ annotations: { readOnlyHint: true },
230
+ handler: async (args, ctx) => {
231
+ return await httpGet(
232
+ { config: ctx.config, toolName: "list_niches" },
233
+ "/v1/niches",
234
+ { currentPage: args.currentPage, pageSize: args.pageSize }
235
+ );
236
+ }
237
+ };
238
+
239
+ // src/tools/get-niche-keywords.ts
240
+ import { z as z2 } from "zod";
241
+ var inputSchema2 = {
242
+ nicheId: z2.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
243
+ };
244
+ var getNicheKeywordsTool = {
245
+ name: "get_niche_keywords",
246
+ title: "Get Master Keyword List for a Niche",
247
+ description: 'Use this when the user asks about keywords, search terms, or search volume for a niche. Retrieves the master keyword list for the specified Niche \u2014 the relevant search terms monitored for ranking and performance metrics. For each keyword, returns search volume, relevancy (numeric score or "Outlier"), and competitor ASIN organic ranks (asinRanks: { ASIN -> rank | null }). Also returns the latestResearchDate.',
248
+ inputSchema: inputSchema2,
249
+ annotations: { readOnlyHint: true },
250
+ handler: async (args, ctx) => {
251
+ return await httpGet(
252
+ { config: ctx.config, toolName: "get_niche_keywords" },
253
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/keywords`
254
+ );
255
+ }
256
+ };
257
+
258
+ // src/tools/get-niche-roots.ts
259
+ import { z as z3 } from "zod";
260
+ var inputSchema3 = {
261
+ nicheId: z3.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
262
+ };
263
+ var getNicheRootsTool = {
264
+ name: "get_niche_roots",
265
+ title: "Get Keyword Roots for a Niche",
266
+ 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.",
267
+ inputSchema: inputSchema3,
268
+ annotations: { readOnlyHint: true },
269
+ handler: async (args, ctx) => {
270
+ return await httpGet(
271
+ { config: ctx.config, toolName: "get_niche_roots" },
272
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/roots`
273
+ );
274
+ }
275
+ };
276
+
277
+ // src/tools/get-niche-competitors.ts
278
+ import { z as z4 } from "zod";
279
+ var inputSchema4 = {
280
+ nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
281
+ };
282
+ var getNicheCompetitorsTool = {
283
+ name: "get_niche_competitors",
284
+ title: "Get Competitors and Niche Statistics",
285
+ 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.",
286
+ inputSchema: inputSchema4,
287
+ annotations: { readOnlyHint: true },
288
+ handler: async (args, ctx) => {
289
+ return await httpGet(
290
+ { config: ctx.config, toolName: "get_niche_competitors" },
291
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/competitors`
292
+ );
293
+ }
294
+ };
295
+
296
+ // src/tools/get-ranking-juice.ts
297
+ import { z as z5 } from "zod";
298
+ var inputSchema5 = {
299
+ nicheId: z5.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
300
+ };
301
+ var getRankingJuiceTool = {
302
+ name: "get_ranking_juice",
303
+ title: "Get Ranking Juices for a Niche",
304
+ 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.",
305
+ inputSchema: inputSchema5,
306
+ annotations: { readOnlyHint: true },
307
+ handler: async (args, ctx) => {
308
+ return await httpGet(
309
+ { config: ctx.config, toolName: "get_ranking_juice" },
310
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/ranking-juices`
311
+ );
312
+ }
313
+ };
314
+
315
+ // src/tools/list-rank-radars.ts
316
+ import { z as z6 } from "zod";
317
+
318
+ // src/types/api.ts
319
+ var ISO_8601 = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/;
320
+ var SUPPORTED_MARKETPLACES = [
321
+ "com",
322
+ "ca",
323
+ "co.uk",
324
+ "com.mx",
325
+ "in",
326
+ "fr",
327
+ "de",
328
+ "es",
329
+ "it",
330
+ "co.jp"
331
+ ];
332
+ var RANK_RADAR_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED"];
333
+ var ALERT_STATUSES = ["active", "resolved", "all"];
334
+ var BILLABLE_FEATURE_TYPES = [
335
+ "DIVED_ASINS",
336
+ "PRODUCT_BRIEF_ASINS",
337
+ "AI_COPYWRITER_PROMPTS",
338
+ "RANK_RADAR_KEYWORDS"
339
+ ];
340
+ var CATALOG_STATUSES = ["Active", "all"];
341
+ var LISTING_CHANGE_TYPES = ["Price", "Content", "Image"];
342
+ var LISTING_CHANGE_SORT_BY = ["date", "type"];
343
+ var SORT_ORDER = ["ASC", "DESC"];
344
+
345
+ // src/tools/list-rank-radars.ts
346
+ var inputSchema6 = {
347
+ currentPage: z6.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
348
+ pageSize: z6.number().int().min(1).max(100).optional().describe("Items per page (max 100). Defaults to 20."),
349
+ nicheId: z6.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
350
+ status: z6.enum(RANK_RADAR_STATUSES).optional().describe(
351
+ "Filter by Rank Radar status. By default, returns only ACTIVE Rank Radars. Allowed: ACTIVE, PAUSED, ARCHIVED."
352
+ ),
353
+ searchText: z6.string().optional().describe("Filter Rank Radars by ASIN or product title.")
354
+ };
355
+ var listRankRadarsTool = {
356
+ name: "list_rank_radars",
357
+ title: "List Rank Radars",
358
+ 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).",
359
+ inputSchema: inputSchema6,
360
+ annotations: { readOnlyHint: true },
361
+ handler: async (args, ctx) => {
362
+ return await httpGet(
363
+ { config: ctx.config, toolName: "list_rank_radars" },
364
+ "/v1/niches/rank-radars",
365
+ {
366
+ currentPage: args.currentPage,
367
+ pageSize: args.pageSize,
368
+ nicheId: args.nicheId,
369
+ status: args.status,
370
+ searchText: args.searchText
371
+ }
372
+ );
373
+ }
374
+ };
375
+
376
+ // src/tools/get-rank-radar-data.ts
377
+ import { z as z7 } from "zod";
378
+ var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
379
+ var inputSchema7 = {
380
+ rankRadarId: z7.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
381
+ 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)."),
382
+ 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.")
383
+ };
384
+ var getRankRadarDataTool = {
385
+ name: "get_rank_radar_data",
386
+ title: "Get Keyword Rankings for a Rank Radar",
387
+ 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`.",
388
+ inputSchema: inputSchema7,
389
+ annotations: { readOnlyHint: true },
390
+ handler: async (args, ctx) => {
391
+ return await httpGet(
392
+ { config: ctx.config, toolName: "get_rank_radar_data" },
393
+ `/v1/niches/rank-radars/${encodeURIComponent(args.rankRadarId)}`,
394
+ { startDate: args.startDate, endDate: args.endDate }
395
+ );
396
+ }
397
+ };
398
+
399
+ // src/tools/create-rank-radar.ts
400
+ import { z as z8 } from "zod";
401
+
402
+ // src/tools/confirm.ts
403
+ function requireConfirmation(confirm, ctx, costNote) {
404
+ if (confirm === true || ctx.config.autoConfirmWrites) return null;
405
+ return {
406
+ status: "confirmation_required",
407
+ message: "This action spends billable tokens and cannot be undone. Confirm the cost with the user, then call again with `confirm: true` to proceed.",
408
+ costNote
409
+ };
410
+ }
411
+
412
+ // src/tools/create-rank-radar.ts
413
+ var inputSchema8 = {
414
+ asin: z8.string().min(1).describe("The ASIN to track keyword rankings for."),
415
+ numberOfKeywords: z8.number().int().min(1).describe("How many keywords to track (minimum 1). More keywords = broader tracking but more Search Term tokens."),
416
+ nicheId: z8.string().min(1).describe("The niche this Rank Radar belongs to. Get one from `list_niches`."),
417
+ confirm: z8.boolean().optional().describe(
418
+ "Must be true to proceed \u2014 creating a Rank Radar spends Search Term tokens. Confirm the cost with the user first."
419
+ )
420
+ };
421
+ var createRankRadarTool = {
422
+ name: "create_rank_radar",
423
+ title: "Create a Rank Radar",
424
+ description: "Use this to start tracking organic and sponsored keyword rankings for an ASIN within a niche. \u26A0\uFE0F Spends Search Term tokens (cost scales with `numberOfKeywords`) and cannot be undone \u2014 set `confirm: true` only after the user approves the cost. Returns the new `rankRadarId`; read its data later with `get_rank_radar_data`. Requires `asin`, `numberOfKeywords` (min 1), and a `nicheId` from `list_niches`.",
425
+ inputSchema: inputSchema8,
426
+ // destructiveHint is technically "additive" here (this creates a new Rank Radar, it
427
+ // doesn't destroy data), but we flag it destructive so clients prompt before the
428
+ // irreversible token spend. The confirm gate enforces this server-side regardless.
429
+ annotations: { readOnlyHint: false, destructiveHint: true },
430
+ handler: async (args, ctx) => {
431
+ const pending = requireConfirmation(
432
+ args.confirm,
433
+ ctx,
434
+ `Creating this Rank Radar consumes Search Term tokens \u2014 one per keyword tracked (${args.numberOfKeywords} requested).`
435
+ );
436
+ if (pending) return pending;
437
+ return await httpPost(
438
+ { config: ctx.config, toolName: "create_rank_radar" },
439
+ "/v1/niches/rank-radars",
440
+ {
441
+ asin: args.asin,
442
+ numberOfKeywords: args.numberOfKeywords,
443
+ nicheId: args.nicheId
444
+ }
445
+ );
446
+ }
447
+ };
448
+
449
+ // src/tools/create-niche-dive.ts
450
+ import { z as z9 } from "zod";
451
+ var inputSchema9 = {
452
+ marketplace: z9.enum(SUPPORTED_MARKETPLACES).describe(
453
+ "Amazon marketplace of the seed ASIN. Note these are full domain suffixes: com, ca, co.uk, com.mx, in, fr, de, es, it, co.jp (e.g. UK is `co.uk`, not `uk`)."
454
+ ),
455
+ asin: z9.string().min(1).describe("The seed ASIN to start the dive from (e.g. B08N5WRWNW). Competitors are built around it."),
456
+ numberOfCompetitors: z9.number().int().min(2).describe("How many competitors to analyze (minimum 2). More competitors = deeper niche but more dive tokens."),
457
+ confirm: z9.boolean().optional().describe("Must be true to proceed \u2014 creating a dive spends dive tokens. Confirm the cost with the user first.")
458
+ };
459
+ var createNicheDiveTool = {
460
+ name: "create_niche_dive",
461
+ title: "Create a Niche Dive",
462
+ description: "Use this to start new niche research from a seed ASIN. \u26A0\uFE0F Spends dive tokens (cost scales with `numberOfCompetitors`) and cannot be undone \u2014 set `confirm: true` only after the user approves the cost. The dive runs asynchronously: this returns immediately with a `diveId` and an `estimatedCompletionDate`. Poll `get_dive_status` with that `diveId` until it reports `success` (which carries the new `nicheId` for use with `list_niches`, `get_niche_keywords`, etc.). Requires `marketplace`, `asin`, and `numberOfCompetitors` (min 2).",
463
+ inputSchema: inputSchema9,
464
+ // destructiveHint is technically "additive" here (a dive creates a new niche, it
465
+ // doesn't destroy data), but we flag it destructive so clients prompt before the
466
+ // irreversible token spend. The confirm gate enforces this server-side regardless.
467
+ annotations: { readOnlyHint: false, destructiveHint: true },
468
+ handler: async (args, ctx) => {
469
+ const pending = requireConfirmation(
470
+ args.confirm,
471
+ ctx,
472
+ `Creating this dive consumes dive tokens \u2014 one batch per competitor analyzed (${args.numberOfCompetitors} requested).`
473
+ );
474
+ if (pending) return pending;
475
+ return await httpPost(
476
+ { config: ctx.config, toolName: "create_niche_dive" },
477
+ "/v1/niches/dives",
478
+ {
479
+ marketplace: args.marketplace,
480
+ asin: args.asin,
481
+ numberOfCompetitors: args.numberOfCompetitors
482
+ }
483
+ );
484
+ }
485
+ };
486
+
487
+ // src/tools/redive-niche.ts
488
+ import { z as z10 } from "zod";
489
+ var inputSchema10 = {
490
+ nicheId: z10.string().min(1).describe("The niche to re-dive. Get one from `list_niches`."),
491
+ mode: z10.enum(["same_competitors", "discover"]).describe(
492
+ "How to pick the competitors for the refreshed dive. `same_competitors` re-runs the niche's current competitor set and takes no other argument \u2014 use it to refresh stale data. `discover` searches for a fresh competitor set, which is what you want when the niche has changed."
493
+ ),
494
+ numberOfCompetitors: z10.number().int().min(2).optional().describe(
495
+ "Total ASINs the refreshed dive should contain, including `lockedAsins`. Required in `discover` mode and rejected in `same_competitors` mode. Dive tokens are spent per ASIN."
496
+ ),
497
+ heroAsin: z10.string().min(1).optional().describe(
498
+ "`discover` mode only. Seed product the competitor discovery starts from. Defaults to the niche's highest-selling competitor, preferring one the user's own connected seller account owns."
499
+ ),
500
+ lockedAsins: z10.array(z10.string().min(1)).optional().describe(
501
+ "`discover` mode only. ASINs that must stay in the competitor set regardless of what discovery finds. Cannot exceed `numberOfCompetitors`, and cannot overlap `excludedAsins`."
502
+ ),
503
+ excludedAsins: z10.array(z10.string().min(1)).optional().describe("`discover` mode only. ASINs discovery must never select."),
504
+ confirm: z10.boolean().optional().describe(
505
+ "Must be true to proceed \u2014 a re-dive spends dive tokens. Confirm the cost with the user first."
506
+ )
507
+ };
508
+ var rediveNicheTool = {
509
+ name: "redive_niche",
510
+ title: "Re-dive an Existing Niche",
511
+ description: "Use this to refresh an existing niche's research with current Amazon data, instead of creating a new niche with `create_niche_dive`. \u26A0\uFE0F Spends dive tokens (one batch per ASIN dived) and cannot be undone \u2014 set `confirm: true` only after the user approves the cost. Two modes: `same_competitors` re-dives the niche's current competitor set (no other argument needed) and `discover` finds a fresh set, sized by `numberOfCompetitors` and steerable with `heroAsin` / `lockedAsins` / `excludedAsins`. Runs asynchronously: returns a `diveId` and an `estimatedCompletionDate` \u2014 poll `get_dive_status` with that `diveId` until it reports `success`. The niche keeps its `nicheId`, so existing rank radars and reports follow the refreshed data.",
512
+ inputSchema: inputSchema10,
513
+ // Same reasoning as create_niche_dive: a re-dive is additive rather than destructive, but the
514
+ // irreversible token spend is what clients need to prompt on. The confirm gate enforces it regardless.
515
+ annotations: { readOnlyHint: false, destructiveHint: true },
516
+ handler: async (args, ctx) => {
517
+ if (args.mode === "discover" && args.numberOfCompetitors === void 0) {
518
+ throw new ApiError(
519
+ "bad_request",
520
+ 400,
521
+ 'numberOfCompetitors is required in `discover` mode \u2014 it is how many ASINs the refreshed dive will contain, and therefore what it costs. Ask the user how many competitors they want (minimum 2), or use mode "same_competitors" to re-dive the niche\'s current set.'
522
+ );
523
+ }
524
+ const pending = requireConfirmation(
525
+ args.confirm,
526
+ ctx,
527
+ args.mode === "discover" ? `Re-diving this niche consumes dive tokens \u2014 one batch per ASIN analyzed (${args.numberOfCompetitors} requested).` : "Re-diving this niche consumes dive tokens \u2014 one batch per competitor currently in the niche. Use `get_niche_competitors` first if the user needs the exact count."
528
+ );
529
+ if (pending) return pending;
530
+ return await httpPost(
531
+ { config: ctx.config, toolName: "redive_niche" },
532
+ `/v1/niches/${encodeURIComponent(args.nicheId)}/redive`,
533
+ {
534
+ mode: args.mode,
535
+ numberOfCompetitors: args.numberOfCompetitors,
536
+ heroAsin: args.heroAsin,
537
+ lockedAsins: args.lockedAsins,
538
+ excludedAsins: args.excludedAsins
539
+ }
540
+ );
541
+ }
542
+ };
543
+
544
+ // src/tools/get-dive-status.ts
545
+ import { z as z11 } from "zod";
546
+ var inputSchema11 = {
547
+ diveId: z11.string().min(1).describe("The dive identifier returned by `create_niche_dive` or `redive_niche`.")
548
+ };
549
+ var getDiveStatusTool = {
550
+ name: "get_dive_status",
551
+ title: "Get Niche Dive Status",
552
+ description: "Use this to poll a niche dive started with `create_niche_dive` or `redive_niche` until it finishes. Returns one of three shapes keyed by `status`: `in_progress` (with `estimatedCompletionDate`), `success` (with the `nicheId` plus `tokensUsed`/`tokensLeft`), or `error` (with an `error` message). On `success`, use the `nicheId` with `list_niches`, `get_niche_keywords`, `get_niche_competitors`, etc. (for a re-dive it is the same niche you passed in, now carrying refreshed data). Re-poll periodically \u2014 dives can take minutes; the `estimatedCompletionDate` hints at when to check.",
553
+ inputSchema: inputSchema11,
554
+ annotations: { readOnlyHint: true },
555
+ handler: async (args, ctx) => {
556
+ return await httpGet(
557
+ { config: ctx.config, toolName: "get_dive_status" },
558
+ `/v1/niches/dives/${encodeURIComponent(args.diveId)}`
559
+ );
560
+ }
561
+ };
562
+
563
+ // src/tools/list-seller-profiles.ts
564
+ import { z as z12 } from "zod";
565
+ var inputSchema12 = {
566
+ currentPage: z12.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
567
+ pageSize: z12.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
568
+ };
569
+ var listSellerProfilesTool = {
570
+ name: "list_seller_profiles",
571
+ title: "List Connected Amazon Seller Profiles",
572
+ description: 'Use this when the user asks which Amazon seller accounts are connected, or as the discovery step to find the `sellerId` and `marketplace` required by `get_asin_inventory_distribution`, `get_seller_catalog`, `get_seller_listing_changes`, and the alert tools. Returns a paginated list of the organization\'s connected seller profiles \u2014 each item has sellerId, sellerName, marketplace (e.g. "com", "co.uk"), hasAdApi (whether Advertising API credentials are connected), and createdAt \u2014 plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).',
573
+ inputSchema: inputSchema12,
574
+ annotations: { readOnlyHint: true },
575
+ handler: async (args, ctx) => {
576
+ return await httpGet(
577
+ { config: ctx.config, toolName: "list_seller_profiles" },
578
+ "/v1/seller_profiles",
579
+ {
580
+ currentPage: args.currentPage,
581
+ pageSize: args.pageSize
582
+ }
583
+ );
584
+ }
585
+ };
586
+
587
+ // src/tools/get-seller-catalog.ts
588
+ import { z as z14 } from "zod";
589
+
590
+ // src/tools/seller-scoped.ts
591
+ import { z as z13 } from "zod";
592
+ var sellerScopedInputSchema = {
593
+ sellerId: z13.string().min(1).describe(
594
+ "Amazon seller account ID. Get it from `list_seller_profiles`, or find it on the Connections page at https://2.datadive.tools."
595
+ ),
596
+ marketplace: z13.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.')
597
+ };
598
+ function sellerMarketplacePath(sellerId, marketplace, suffix) {
599
+ return `/v1/seller_profiles/${encodeURIComponent(sellerId)}/marketplaces/${encodeURIComponent(
600
+ marketplace
601
+ )}/${suffix}`;
602
+ }
603
+
604
+ // src/tools/get-seller-catalog.ts
605
+ var inputSchema13 = {
606
+ ...sellerScopedInputSchema,
607
+ search: z14.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
608
+ brand: z14.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
609
+ status: z14.enum(CATALOG_STATUSES).optional().describe('Listing status filter. "Active" (default) or "all".'),
610
+ currentPage: z14.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
611
+ pageSize: z14.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
612
+ };
613
+ var getSellerCatalogTool = {
614
+ name: "get_seller_catalog",
615
+ title: "List Catalog ASINs for a Seller",
616
+ description: "Use this when the user wants to browse or search a seller's own Amazon catalog \u2014 the products they sell on a given marketplace. Requires a `sellerId` + `marketplace` (use `list_seller_profiles` to discover them). Filter by `search` (title/brand), `brand`, and `status` (Active by default, or all). Returns a paginated list where each item has asin, title, parentAsin, brand, status, imageUrl, and hasVariations, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
617
+ inputSchema: inputSchema13,
618
+ annotations: { readOnlyHint: true },
619
+ handler: async (args, ctx) => {
620
+ return await httpGet(
621
+ { config: ctx.config, toolName: "get_seller_catalog" },
622
+ sellerMarketplacePath(args.sellerId, args.marketplace, "catalog"),
623
+ {
624
+ search: args.search,
625
+ brand: args.brand,
626
+ status: args.status,
627
+ currentPage: args.currentPage,
628
+ pageSize: args.pageSize
629
+ }
630
+ );
631
+ }
632
+ };
633
+
634
+ // src/tools/get-seller-listing-changes.ts
635
+ import { z as z15 } from "zod";
636
+ var inputSchema14 = {
637
+ ...sellerScopedInputSchema,
638
+ types: z15.array(z15.enum(LISTING_CHANGE_TYPES)).min(1).optional().describe('Filter to specific change types. Omit for all. Any of "Price", "Content", "Image".'),
639
+ asin: z15.string().min(1).optional().describe("Filter to an exact ASIN or parent ASIN."),
640
+ brand: z15.string().min(1).optional().describe("Case-insensitive partial match on brand name."),
641
+ search: z15.string().min(1).optional().describe("Case-insensitive partial match on product title or brand."),
642
+ startDate: z15.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 changes detected on or after this time."),
643
+ endDate: z15.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 changes detected on or before this time."),
644
+ sortBy: z15.enum(LISTING_CHANGE_SORT_BY).optional().describe('Sort field. "date" (default) or "type".'),
645
+ sortOrder: z15.enum(SORT_ORDER).optional().describe('Sort direction. "DESC" (default) or "ASC".'),
646
+ includeCorrelations: z15.boolean().optional().describe(
647
+ "When true, include the ranking/conversion `correlation` object per change (CVR and search-term movement before vs after). Defaults to false."
648
+ ),
649
+ currentPage: z15.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
650
+ pageSize: z15.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
651
+ };
652
+ var getSellerListingChangesTool = {
653
+ name: "get_seller_listing_changes",
654
+ title: "List Listing Changes for a Seller",
655
+ description: "Use this when the user asks what changed on their Amazon listings \u2014 price, content (title/bullets/description), or image edits \u2014 for a seller account. Requires a `sellerId` + `marketplace` (use `list_seller_profiles` to discover them). Filter by `types`, `asin`/parent ASIN, `brand`, `search`, and a `startDate`/`endDate` range; sort with `sortBy`/`sortOrder`. Set `includeCorrelations: true` to attach the ranking/conversion impact per change. Returns a paginated list where each item has asin, title, imageUrl, date, type, contentType, description, previousValue, newValue, and (optionally) correlation, plus pagination metadata (currentPage, pageSize, total, lastPage, hasNext, hasPrev).",
656
+ inputSchema: inputSchema14,
657
+ annotations: { readOnlyHint: true },
658
+ handler: async (args, ctx) => {
659
+ return await httpGet(
660
+ { config: ctx.config, toolName: "get_seller_listing_changes" },
661
+ sellerMarketplacePath(args.sellerId, args.marketplace, "listing-changes"),
662
+ {
663
+ // The API accepts `types` as a comma-separated list.
664
+ types: args.types?.join(","),
665
+ asin: args.asin,
666
+ brand: args.brand,
667
+ search: args.search,
668
+ startDate: args.startDate,
669
+ endDate: args.endDate,
670
+ sortBy: args.sortBy,
671
+ sortOrder: args.sortOrder,
672
+ includeCorrelations: args.includeCorrelations,
673
+ currentPage: args.currentPage,
674
+ pageSize: args.pageSize
675
+ }
676
+ );
677
+ }
678
+ };
679
+
680
+ // src/tools/get-asin-inventory-distribution.ts
681
+ import { z as z16 } from "zod";
682
+ var inputSchema15 = {
683
+ sellerId: z16.string().min(1).describe(
684
+ "Amazon seller account ID as connected to DataDive. Get it from `list_seller_profiles`, or find it on the Connections page at https://2.datadive.tools."
685
+ ),
686
+ marketplace: z16.enum(SUPPORTED_MARKETPLACES).describe('Amazon marketplace code, e.g. "com" for amazon.com or "co.uk" for amazon.co.uk.'),
687
+ asin: z16.string().regex(/^[A-Z0-9]{10}$/, "ASIN must be 10 uppercase alphanumeric characters (e.g. B08XYZ1234).").describe(
688
+ "Amazon Standard Identification Number (10 uppercase alphanumeric characters, e.g. B08XYZ1234)."
689
+ )
690
+ };
691
+ var getAsinInventoryDistributionTool = {
692
+ name: "get_asin_inventory_distribution",
693
+ title: "Get Per-Fulfillment-Center Inventory Distribution for an ASIN",
694
+ 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 `list_seller_profiles` or the user's DataDive Connections page at 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.",
695
+ inputSchema: inputSchema15,
696
+ annotations: { readOnlyHint: true },
697
+ handler: async (args, ctx) => {
698
+ return await httpGet(
699
+ { config: ctx.config, toolName: "get_asin_inventory_distribution" },
700
+ `/v1/sellers/${encodeURIComponent(args.sellerId)}/marketplaces/${encodeURIComponent(
701
+ args.marketplace
702
+ )}/asins/${encodeURIComponent(args.asin)}/inventory`
703
+ );
704
+ }
705
+ };
706
+
707
+ // src/tools/alert-query.ts
708
+ import { z as z17 } from "zod";
709
+ var alertsQueryInputSchema = {
710
+ sellerId: z17.string().min(1).max(50).optional().describe('Filter to a single connected Amazon seller account ID, e.g. "A1B2C3D4E5".'),
711
+ marketplace: z17.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.'),
712
+ status: z17.enum(ALERT_STATUSES).optional().describe(
713
+ "Lifecycle filter. `active` (default) returns unresolved alerts, `resolved` returns resolved alerts, `all` returns both. Dismissed alerts are never returned."
714
+ ),
715
+ updatedSince: z17.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(
716
+ "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."
717
+ ),
718
+ currentPage: z17.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
719
+ pageSize: z17.number().int().min(1).max(50).optional().describe("Items per page (max 50). Defaults to 20.")
720
+ };
721
+
722
+ // src/tools/list-indexing-issue-alerts.ts
723
+ var listIndexingIssueAlertsTool = {
724
+ name: "list_indexing_issue_alerts",
725
+ title: "List Indexing-Issue Alerts",
726
+ 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).",
727
+ inputSchema: alertsQueryInputSchema,
728
+ annotations: { readOnlyHint: true },
729
+ handler: async (args, ctx) => {
730
+ return await httpGet(
731
+ { config: ctx.config, toolName: "list_indexing_issue_alerts" },
732
+ "/v1/alerts/indexing-issues",
733
+ {
734
+ sellerId: args.sellerId,
735
+ marketplace: args.marketplace,
736
+ status: args.status,
737
+ updatedSince: args.updatedSince,
738
+ currentPage: args.currentPage,
739
+ pageSize: args.pageSize
740
+ }
741
+ );
742
+ }
743
+ };
744
+
745
+ // src/tools/list-blind-spend-alerts.ts
746
+ var listBlindSpendAlertsTool = {
747
+ name: "list_blind_spend_alerts",
748
+ title: "List Blind-Spend (Wasted-Spend) Alerts",
749
+ 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).",
750
+ inputSchema: alertsQueryInputSchema,
751
+ annotations: { readOnlyHint: true },
752
+ handler: async (args, ctx) => {
753
+ return await httpGet(
754
+ { config: ctx.config, toolName: "list_blind_spend_alerts" },
755
+ "/v1/alerts/blind-spend",
756
+ {
757
+ sellerId: args.sellerId,
758
+ marketplace: args.marketplace,
759
+ status: args.status,
760
+ updatedSince: args.updatedSince,
761
+ currentPage: args.currentPage,
762
+ pageSize: args.pageSize
763
+ }
764
+ );
765
+ }
766
+ };
767
+
768
+ // src/tools/get-quota.ts
769
+ var inputSchema16 = {};
770
+ var getQuotaTool = {
771
+ name: "get_quota",
772
+ title: "Get Quota Usage",
773
+ 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.",
774
+ inputSchema: inputSchema16,
775
+ annotations: { readOnlyHint: true },
776
+ handler: async (_args, ctx) => {
777
+ return await httpGet({ config: ctx.config, toolName: "get_quota" }, "/v1/quota");
778
+ }
779
+ };
780
+
781
+ // src/tools/list-usage.ts
782
+ import { z as z18 } from "zod";
783
+ var inputSchema17 = {
784
+ type: z18.enum(BILLABLE_FEATURE_TYPES).optional().describe("Filter to a single billable feature type. Omit to return all types."),
785
+ search: z18.string().min(1).optional().describe("Case-insensitive partial match on the user's name or email."),
786
+ startDate: z18.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."),
787
+ endDate: z18.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."),
788
+ currentPage: z18.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
789
+ pageSize: z18.number().int().min(1).max(200).optional().describe("Items per page (max 200). Defaults to 50.")
790
+ };
791
+ var listUsageTool = {
792
+ name: "list_usage",
793
+ title: "List Billable Usage Logs",
794
+ 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).",
795
+ inputSchema: inputSchema17,
796
+ annotations: { readOnlyHint: true },
797
+ handler: async (args, ctx) => {
798
+ return await httpGet(
799
+ { config: ctx.config, toolName: "list_usage" },
800
+ "/v1/usage",
801
+ {
802
+ type: args.type,
803
+ search: args.search,
804
+ startDate: args.startDate,
805
+ endDate: args.endDate,
806
+ currentPage: args.currentPage,
807
+ pageSize: args.pageSize
808
+ }
809
+ );
810
+ }
811
+ };
812
+
813
+ // src/tools/index.ts
814
+ var allTools = [
815
+ listNichesTool,
816
+ getNicheKeywordsTool,
817
+ getNicheRootsTool,
818
+ getNicheCompetitorsTool,
819
+ getRankingJuiceTool,
820
+ listRankRadarsTool,
821
+ getRankRadarDataTool,
822
+ createRankRadarTool,
823
+ createNicheDiveTool,
824
+ rediveNicheTool,
825
+ getDiveStatusTool,
826
+ listSellerProfilesTool,
827
+ getSellerCatalogTool,
828
+ getSellerListingChangesTool,
829
+ getAsinInventoryDistributionTool,
830
+ listIndexingIssueAlertsTool,
831
+ listBlindSpendAlertsTool,
832
+ getQuotaTool,
833
+ listUsageTool
834
+ ];
835
+
836
+ // src/server.ts
837
+ function requiredScope(tool) {
838
+ return tool.annotations?.readOnlyHint === true ? SCOPE_READ : SCOPE_WRITE;
839
+ }
840
+ function buildServer(config) {
841
+ const server = new McpServer({
842
+ name: "datadive",
843
+ version: PKG_VERSION
844
+ });
845
+ for (const tool of allTools) {
846
+ server.registerTool(
847
+ tool.name,
848
+ {
849
+ title: tool.title,
850
+ description: tool.description,
851
+ inputSchema: tool.inputSchema,
852
+ annotations: tool.annotations
853
+ },
854
+ async (args) => {
855
+ const scope = requiredScope(tool);
856
+ if (config.scopes && !config.scopes.includes(scope)) {
857
+ return {
858
+ isError: true,
859
+ content: [
860
+ {
861
+ type: "text",
862
+ text: `Insufficient scope: this tool requires the "${scope}" scope, which was not granted to this connection. Reconnect DataDive and enable the missing permission to use it.`
863
+ }
864
+ ]
865
+ };
866
+ }
867
+ try {
868
+ const data = await tool.handler(args, { config });
869
+ const content = [
870
+ {
871
+ type: "text",
872
+ text: JSON.stringify(data, null, 2)
873
+ }
874
+ ];
875
+ const notice = takeUpgradeNotice();
876
+ if (notice) {
877
+ content.push({
878
+ type: "text",
879
+ text: `\u26A0\uFE0F datadive-mcp update available \u2014 ${notice}`
880
+ });
881
+ }
882
+ return { content };
883
+ } catch (err) {
884
+ return {
885
+ isError: true,
886
+ content: [
887
+ {
888
+ type: "text",
889
+ text: err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err)
890
+ }
891
+ ]
892
+ };
893
+ }
894
+ }
895
+ );
896
+ }
897
+ return server;
898
+ }
899
+ export {
900
+ ApiError,
901
+ PKG_VERSION,
902
+ SCOPE_READ,
903
+ SCOPE_WRITE,
904
+ allTools,
905
+ buildServer,
906
+ loadConfig,
907
+ requiredScope
908
+ };
909
+ //# sourceMappingURL=lib.js.map