@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/dist/lib.d.ts CHANGED
@@ -157,8 +157,9 @@ declare const allTools: ReadonlyArray<AnyTool>;
157
157
  * Source of truth for status semantics:
158
158
  * datadive-backend/src/external-api/auth/api-key.guard.ts (401/402 logic)
159
159
  * datadive-backend/src/external-api/external-api-v1.controller.ts (per-route docs)
160
+ * datadive-backend/src/microservices/token/quota-exceeded.error.ts (QUOTA_EXCEEDED body)
160
161
  */
161
- type ApiErrorKind = "auth" | "payment" | "forbidden" | "not_found" | "bad_request" | "rate_limit" | "server" | "network" | "http";
162
+ type ApiErrorKind = "auth" | "payment" | "quota" | "forbidden" | "not_found" | "bad_request" | "rate_limit" | "server" | "network" | "http";
162
163
  declare class ApiError extends Error {
163
164
  readonly kind: ApiErrorKind;
164
165
  readonly status: number;
package/dist/lib.js CHANGED
@@ -26,6 +26,13 @@ import { z } from "zod";
26
26
 
27
27
  // src/http/errors.ts
28
28
  var KEY_HELP_URL2 = "https://2.datadive.tools/api-key";
29
+ var FEATURE_LABELS = {
30
+ DIVED_ASINS: "Dive tokens",
31
+ RANK_RADAR_KEYWORDS: "Rank Radar tracked keywords",
32
+ PRODUCT_BRIEF_ASINS: "Product Brief ASINs",
33
+ AI_COPYWRITER_PROMPTS: "AI Copywriter prompts",
34
+ INDEXING_DIAGNOSIS: "Indexing Diagnosis checks"
35
+ };
29
36
  var ApiError = class _ApiError extends Error {
30
37
  kind;
31
38
  status;
@@ -38,6 +45,9 @@ var ApiError = class _ApiError extends Error {
38
45
  this.body = body;
39
46
  }
40
47
  static fromHttp(status, body) {
48
+ if (isQuotaExceededBody(body)) {
49
+ return new _ApiError("quota", status, quotaExceededMessage(body), body);
50
+ }
41
51
  const serverMsg = extractMessage(body);
42
52
  if (status === 400) {
43
53
  return new _ApiError(
@@ -88,6 +98,35 @@ var ApiError = class _ApiError extends Error {
88
98
  return new _ApiError("http", status, serverMsg ?? `HTTP ${status}`, body);
89
99
  }
90
100
  };
101
+ function isQuotaExceededBody(body) {
102
+ if (!body || typeof body !== "object") return false;
103
+ const b = body;
104
+ return b.error === "QUOTA_EXCEEDED" && typeof b.subscriptionUrl === "string";
105
+ }
106
+ function quotaExceededMessage(body) {
107
+ const lead = typeof body.message === "string" && body.message.length > 0 ? body.message : "Quota exceeded";
108
+ const feature = body.feature ? FEATURE_LABELS[body.feature] ?? body.feature : null;
109
+ const parts = [];
110
+ if (feature) {
111
+ parts.push(`This subscription has used up its ${feature} quota${usageSuffix(body)}.`);
112
+ } else {
113
+ parts.push(`This subscription has used up its quota for this action${usageSuffix(body)}.`);
114
+ }
115
+ if (body.nextRefreshDate) {
116
+ parts.push(`It refreshes on ${body.nextRefreshDate.slice(0, 10)}.`);
117
+ }
118
+ parts.push(
119
+ `Tell the user which quota ran out and that they can review or raise it on their subscription page: ${body.subscriptionUrl}`
120
+ );
121
+ parts.push("Do not retry this call until the quota has been raised.");
122
+ return `${lead}. ${parts.join(" ")}`;
123
+ }
124
+ function usageSuffix(body) {
125
+ if (typeof body.used === "number" && typeof body.capacity === "number") {
126
+ return ` (${body.used} of ${body.capacity} used)`;
127
+ }
128
+ return "";
129
+ }
91
130
  function extractMessage(body) {
92
131
  if (!body || typeof body !== "object") return null;
93
132
  const b = body;
@@ -100,7 +139,7 @@ function extractMessage(body) {
100
139
  }
101
140
 
102
141
  // package.json
103
- var version = "0.11.0";
142
+ var version = "0.12.0";
104
143
 
105
144
  // src/http/client.ts
106
145
  var PKG_VERSION = version;
@@ -236,20 +275,36 @@ function isObject(v) {
236
275
  // src/tools/list-niches.ts
237
276
  var LIST_NICHES_MAX_PAGE_SIZE = 50;
238
277
  var inputSchema = {
239
- currentPage: z.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
240
- 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.`)
278
+ currentPage: z.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1. Keep requesting the next page while `hasNext` is true."),
279
+ 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.`),
280
+ searchText: z.string().trim().min(1).max(200).optional().describe(
281
+ "Case-insensitive partial match on the niche label or hero keyword. Prefer this over paging through everything when the user names a niche."
282
+ ),
283
+ 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)."),
284
+ orderBy: z.enum(["lastDived", "name"]).optional().describe(
285
+ "Sort field: `lastDived` (by latestResearchDate, newest first by default) or `name` (by nicheLabel, A\u2192Z by default). Defaults to `lastDived`."
286
+ ),
287
+ sortOrder: z.enum(["ASC", "DESC"]).optional().describe("Sort direction. Defaults to DESC for `lastDived` and ASC for `name`.")
241
288
  };
242
289
  var listNichesTool = {
243
290
  name: "list_niches",
244
291
  title: "List DataDive Niches",
245
- 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.",
292
+ 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.`,
246
293
  inputSchema,
247
294
  annotations: { readOnlyHint: true },
248
295
  handler: async (args, ctx) => {
249
- return await httpGet({ config: ctx.config, toolName: "list_niches" }, "/v1/niches", {
250
- currentPage: args.currentPage,
251
- pageSize: args.pageSize
252
- });
296
+ return await httpGet(
297
+ { config: ctx.config, toolName: "list_niches" },
298
+ "/v1/niches",
299
+ {
300
+ currentPage: args.currentPage,
301
+ pageSize: args.pageSize,
302
+ searchText: args.searchText,
303
+ searchAsin: args.searchAsin,
304
+ orderBy: args.orderBy,
305
+ sortOrder: args.sortOrder
306
+ }
307
+ );
253
308
  }
254
309
  };
255
310