@datadive-tools/mcp 0.11.0 → 0.12.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
@@ -197,6 +197,7 @@ specific HTTP status from the DataDive API.
197
197
  |---|---|
198
198
  | **Authentication failed: your DATADIVE_API_KEY is invalid or expired** | The key is wrong, deleted, or expired. Generate a new one at https://2.datadive.tools/api-key. |
199
199
  | **Subscription is inactive or paused** | Resume billing at https://2.datadive.tools — the API key is valid but the subscription isn't active. |
200
+ | **Quota exceeded** / **… quota exceeded** | The subscription's quota for that billable feature (Dive tokens, Rank Radar tracked keywords, AI Copywriter prompts, …) is used up. The message names the feature, the usage, the next refresh date when there is one, and links to the subscription overview page where it can be raised. Don't retry until it has been. |
200
201
  | **Forbidden** | The key is valid but doesn't have access to that resource. Usually a niche/rank-radar that belongs to a different org. |
201
202
  | **Rate limit exceeded** | Wait a few seconds and retry. |
202
203
  | **Bad request** | Check the parameters — the message echoes the server's validation error (e.g., `pageSize must not exceed 50`). |
package/dist/index.js CHANGED
@@ -31,6 +31,13 @@ import { z } from "zod";
31
31
 
32
32
  // src/http/errors.ts
33
33
  var KEY_HELP_URL2 = "https://2.datadive.tools/api-key";
34
+ var FEATURE_LABELS = {
35
+ DIVED_ASINS: "Dive tokens",
36
+ RANK_RADAR_KEYWORDS: "Rank Radar tracked keywords",
37
+ PRODUCT_BRIEF_ASINS: "Product Brief ASINs",
38
+ AI_COPYWRITER_PROMPTS: "AI Copywriter prompts",
39
+ INDEXING_DIAGNOSIS: "Indexing Diagnosis checks"
40
+ };
34
41
  var ApiError = class _ApiError extends Error {
35
42
  kind;
36
43
  status;
@@ -43,6 +50,9 @@ var ApiError = class _ApiError extends Error {
43
50
  this.body = body;
44
51
  }
45
52
  static fromHttp(status, body) {
53
+ if (isQuotaExceededBody(body)) {
54
+ return new _ApiError("quota", status, quotaExceededMessage(body), body);
55
+ }
46
56
  const serverMsg = extractMessage(body);
47
57
  if (status === 400) {
48
58
  return new _ApiError(
@@ -93,6 +103,35 @@ var ApiError = class _ApiError extends Error {
93
103
  return new _ApiError("http", status, serverMsg ?? `HTTP ${status}`, body);
94
104
  }
95
105
  };
106
+ function isQuotaExceededBody(body) {
107
+ if (!body || typeof body !== "object") return false;
108
+ const b = body;
109
+ return b.error === "QUOTA_EXCEEDED" && typeof b.subscriptionUrl === "string";
110
+ }
111
+ function quotaExceededMessage(body) {
112
+ const lead = typeof body.message === "string" && body.message.length > 0 ? body.message : "Quota exceeded";
113
+ const feature = body.feature ? FEATURE_LABELS[body.feature] ?? body.feature : null;
114
+ const parts = [];
115
+ if (feature) {
116
+ parts.push(`This subscription has used up its ${feature} quota${usageSuffix(body)}.`);
117
+ } else {
118
+ parts.push(`This subscription has used up its quota for this action${usageSuffix(body)}.`);
119
+ }
120
+ if (body.nextRefreshDate) {
121
+ parts.push(`It refreshes on ${body.nextRefreshDate.slice(0, 10)}.`);
122
+ }
123
+ parts.push(
124
+ `Tell the user which quota ran out and that they can review or raise it on their subscription page: ${body.subscriptionUrl}`
125
+ );
126
+ parts.push("Do not retry this call until the quota has been raised.");
127
+ return `${lead}. ${parts.join(" ")}`;
128
+ }
129
+ function usageSuffix(body) {
130
+ if (typeof body.used === "number" && typeof body.capacity === "number") {
131
+ return ` (${body.used} of ${body.capacity} used)`;
132
+ }
133
+ return "";
134
+ }
96
135
  function extractMessage(body) {
97
136
  if (!body || typeof body !== "object") return null;
98
137
  const b = body;
@@ -105,7 +144,7 @@ function extractMessage(body) {
105
144
  }
106
145
 
107
146
  // package.json
108
- var version = "0.11.0";
147
+ var version = "0.12.0";
109
148
 
110
149
  // src/http/client.ts
111
150
  var PKG_VERSION = version;
@@ -241,20 +280,36 @@ function isObject(v) {
241
280
  // src/tools/list-niches.ts
242
281
  var LIST_NICHES_MAX_PAGE_SIZE = 50;
243
282
  var inputSchema = {
244
- currentPage: z.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
245
- pageSize: z.number().int().min(1).max(LIST_NICHES_MAX_PAGE_SIZE).optional().describe(`Items per page (max ${LIST_NICHES_MAX_PAGE_SIZE}). Defaults to 20.`)
283
+ currentPage: z.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1. Keep requesting the next page while `hasNext` is true."),
284
+ pageSize: z.number().int().min(1).max(LIST_NICHES_MAX_PAGE_SIZE).optional().describe(`Items per page (max ${LIST_NICHES_MAX_PAGE_SIZE}). Defaults to 20.`),
285
+ searchText: z.string().trim().min(1).max(200).optional().describe(
286
+ "Case-insensitive partial match on the niche label or hero keyword. Prefer this over paging through everything when the user names a niche."
287
+ ),
288
+ searchAsin: z.string().trim().regex(/^[A-Za-z0-9]{10}$/, "must be a 10-character ASIN").optional().describe("Only niches whose competitor set contains this ASIN (10 characters, e.g. B08N5WRWNW)."),
289
+ orderBy: z.enum(["lastDived", "name"]).optional().describe(
290
+ "Sort field: `lastDived` (by latestResearchDate, newest first by default) or `name` (by nicheLabel, A\u2192Z by default). Defaults to `lastDived`."
291
+ ),
292
+ sortOrder: z.enum(["ASC", "DESC"]).optional().describe("Sort direction. Defaults to DESC for `lastDived` and ASC for `name`.")
246
293
  };
247
294
  var listNichesTool = {
248
295
  name: "list_niches",
249
296
  title: "List DataDive Niches",
250
- 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 the Niches of the account. 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). TEMPORARY LIMITATION: the API does not apply paging yet, so a single call returns every niche of the account whatever `currentPage` and `pageSize` say, and the pagination metadata describes a page that was not applied. Read the whole `data` array and filter it yourself; do not walk `currentPage` while `hasNext` is true, because every page repeats the same rows. Accounts with many niches therefore return a large response. There is no server-side search or sorting.",
297
+ 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, newest dive first. 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). Narrow with \`searchText\` (label/keyword) or \`searchAsin\` (a competitor ASIN) instead of scanning pages; a page holds at most ${LIST_NICHES_MAX_PAGE_SIZE} niches, so the full list of a large account needs several calls.`,
251
298
  inputSchema,
252
299
  annotations: { readOnlyHint: true },
253
300
  handler: async (args, ctx) => {
254
- return await httpGet({ config: ctx.config, toolName: "list_niches" }, "/v1/niches", {
255
- currentPage: args.currentPage,
256
- pageSize: args.pageSize
257
- });
301
+ return await httpGet(
302
+ { config: ctx.config, toolName: "list_niches" },
303
+ "/v1/niches",
304
+ {
305
+ currentPage: args.currentPage,
306
+ pageSize: args.pageSize,
307
+ searchText: args.searchText,
308
+ searchAsin: args.searchAsin,
309
+ orderBy: args.orderBy,
310
+ sortOrder: args.sortOrder
311
+ }
312
+ );
258
313
  }
259
314
  };
260
315