@datadive-tools/mcp 0.1.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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/index.js +363 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DataDive
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @datadive-tools/mcp
|
|
2
|
+
|
|
3
|
+
An [MCP](https://modelcontextprotocol.io) server that lets Claude (or any
|
|
4
|
+
MCP-compatible client) query your DataDive niches, keywords, competitors, and
|
|
5
|
+
Rank Radar data using your existing API key.
|
|
6
|
+
|
|
7
|
+
Runs locally on your machine over stdio. Your API key never leaves your machine
|
|
8
|
+
except as the `x-api-key` header on requests to `api.datadive.tools`.
|
|
9
|
+
|
|
10
|
+
## What you can ask
|
|
11
|
+
|
|
12
|
+
- "List my DataDive niches in marketplace `com`."
|
|
13
|
+
- "What's the master keyword list for niche `z515cGOFg3`?"
|
|
14
|
+
- "Who are the top competitors in niche X and what are their sales?"
|
|
15
|
+
- "What's my ranking juice for niche X — where can I improve?"
|
|
16
|
+
- "Show me my rank radars."
|
|
17
|
+
- "Plot the organic ranking trend for rank radar Y from 2024-03-01 to 2024-04-01."
|
|
18
|
+
|
|
19
|
+
## 1. Get a DataDive API key
|
|
20
|
+
|
|
21
|
+
1. Sign in at [https://2.datadive.tools](https://2.datadive.tools/sign-in).
|
|
22
|
+
2. Go to **Settings → API Key** (`/api-key`).
|
|
23
|
+
3. Click **Generate API Key**. Copy the value — you'll paste it into your MCP
|
|
24
|
+
client config below.
|
|
25
|
+
|
|
26
|
+
> Requires the **Billing Manager** or **Owner** role and the **Standard plan
|
|
27
|
+
> or higher**. Contact your org admin if you can't see the page.
|
|
28
|
+
|
|
29
|
+
## 2. Add it to your MCP client
|
|
30
|
+
|
|
31
|
+
### Claude Desktop
|
|
32
|
+
|
|
33
|
+
Edit your config file:
|
|
34
|
+
|
|
35
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
36
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
37
|
+
|
|
38
|
+
Add the `datadive` entry under `mcpServers`:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"datadive": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@datadive-tools/mcp"],
|
|
46
|
+
"env": {
|
|
47
|
+
"DATADIVE_API_KEY": "ddk_live_..."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Restart Claude Desktop. You should see `datadive` in the tools menu.
|
|
55
|
+
|
|
56
|
+
### Claude Code
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
claude mcp add datadive -- npx -y @datadive-tools/mcp
|
|
60
|
+
# Then add the env var via:
|
|
61
|
+
# claude mcp add datadive --env DATADIVE_API_KEY=ddk_live_... -- npx -y @datadive-tools/mcp
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or edit `.mcp.json` in your project / `~/.claude/mcp.json` globally with the
|
|
65
|
+
same JSON shape as above.
|
|
66
|
+
|
|
67
|
+
### Cursor
|
|
68
|
+
|
|
69
|
+
**Settings → MCP → Add new MCP server** and paste the same JSON shape:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"datadive": {
|
|
74
|
+
"command": "npx",
|
|
75
|
+
"args": ["-y", "@datadive-tools/mcp"],
|
|
76
|
+
"env": { "DATADIVE_API_KEY": "ddk_live_..." }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 3. Verify
|
|
82
|
+
|
|
83
|
+
Ask Claude: **"List my DataDive niches."**
|
|
84
|
+
|
|
85
|
+
You should see a tool call to `list_niches` and a JSON response with your
|
|
86
|
+
niches, plus pagination metadata. If you don't, see Troubleshooting below.
|
|
87
|
+
|
|
88
|
+
## Available tools (v0.1)
|
|
89
|
+
|
|
90
|
+
| Tool | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `list_niches` | Paginated list of your niches. Discovery step — returns `nicheId`s for the next four tools. |
|
|
93
|
+
| `get_niche_keywords` | Master keyword list for a niche: search volume, relevancy, competitor ASIN ranks. |
|
|
94
|
+
| `get_niche_competitors` | Competitor ASINs and niche statistics (sales, revenue, ratings, opportunity score). |
|
|
95
|
+
| `get_ranking_juice` | DataDive proprietary ranking-juice metric per competitor (current vs optimized listing). |
|
|
96
|
+
| `list_rank_radars` | Paginated list of rank radars. Filter by `nicheId` or `status`. |
|
|
97
|
+
| `get_rank_radar_data` | Historical keyword rankings for a rank radar within a `startDate`/`endDate` range. |
|
|
98
|
+
|
|
99
|
+
Read-only. All data is scoped to the organization that owns the API key.
|
|
100
|
+
|
|
101
|
+
> **Coming in v1.1:** keyword roots, async niche dives with polling, rank-radar
|
|
102
|
+
> creation, AI copywriter. We'll prioritize based on usage signal — if you
|
|
103
|
+
> have a request, open an issue.
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
| Env var | Required | Default | Notes |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| `DATADIVE_API_KEY` | yes | — | Generate at https://2.datadive.tools/api-key |
|
|
110
|
+
| `DATADIVE_API_BASE_URL` | no | `https://api.datadive.tools` | Override for staging |
|
|
111
|
+
|
|
112
|
+
## Troubleshooting
|
|
113
|
+
|
|
114
|
+
If a tool call returns an error message, it'll be one of these — each maps to a
|
|
115
|
+
specific HTTP status from the DataDive API.
|
|
116
|
+
|
|
117
|
+
| Message starts with… | What to do |
|
|
118
|
+
|---|---|
|
|
119
|
+
| **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. |
|
|
120
|
+
| **Subscription is inactive or paused** | Resume billing at https://2.datadive.tools — the API key is valid but the subscription isn't active. |
|
|
121
|
+
| **Forbidden** | The key is valid but doesn't have access to that resource. Usually a niche/rank-radar that belongs to a different org. |
|
|
122
|
+
| **Rate limit exceeded** | Wait a few seconds and retry. |
|
|
123
|
+
| **Bad request** | Check the parameters — the message echoes the server's validation error (e.g., `pageSize must not exceed 100`). |
|
|
124
|
+
| **DataDive API error (5xx)** | Transient backend issue. Try again; if it persists, contact support. |
|
|
125
|
+
| **Network error reaching …** | Your machine can't reach `api.datadive.tools` — check VPN / firewall / DNS. |
|
|
126
|
+
|
|
127
|
+
If the server itself fails to start, look in your MCP client's log output:
|
|
128
|
+
|
|
129
|
+
- **`DATADIVE_API_KEY environment variable is required`** — the env var isn't
|
|
130
|
+
being passed through to the binary. Confirm your client config has it under
|
|
131
|
+
`env` (not `args`), and that you've restarted the client after editing.
|
|
132
|
+
|
|
133
|
+
## Privacy
|
|
134
|
+
|
|
135
|
+
- The MCP server runs locally on your machine. It is a thin shim — every tool
|
|
136
|
+
call becomes a single HTTPS request from your machine to
|
|
137
|
+
`api.datadive.tools` with your API key as the `x-api-key` header.
|
|
138
|
+
- Your API key is stored only in your MCP client's config file (which you
|
|
139
|
+
control). It is never sent anywhere else.
|
|
140
|
+
- The server logs nothing on its own. Standard backend access logs at DataDive
|
|
141
|
+
record per-request metadata (organization, route, status, latency) the same
|
|
142
|
+
way any direct API call would.
|
|
143
|
+
|
|
144
|
+
## Development
|
|
145
|
+
|
|
146
|
+
```sh
|
|
147
|
+
npm install
|
|
148
|
+
npm run build # tsup -> dist/index.js (with shebang)
|
|
149
|
+
npm test # vitest run
|
|
150
|
+
npm run typecheck # tsc --noEmit
|
|
151
|
+
npm run lint # eslint
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Smoke test against staging:
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
DATADIVE_SMOKE_API_KEY=ddk_test_... DATADIVE_API_BASE_URL=https://api-staging.datadive.tools npm run test:smoke
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Test the binary end-to-end with the MCP Inspector:
|
|
161
|
+
|
|
162
|
+
```sh
|
|
163
|
+
DATADIVE_API_KEY=ddk_live_... npx @modelcontextprotocol/inspector dist/index.js
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Releasing
|
|
167
|
+
|
|
168
|
+
The repo uses [Changesets](https://github.com/changesets/changesets):
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npx changeset # add a changeset describing the change (patch/minor/major)
|
|
172
|
+
git commit -am "fix: ..."
|
|
173
|
+
git push
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A "Version Packages" PR opens automatically. Merging it bumps the version,
|
|
177
|
+
updates CHANGELOG.md, and publishes to npm with provenance.
|
|
178
|
+
|
|
179
|
+
## Support
|
|
180
|
+
|
|
181
|
+
- **Issues:** https://github.com/Data-Dive-Tools/datadive-mcp/issues
|
|
182
|
+
- **API docs:** https://developer.datadive.tools/docs#/v1
|
|
183
|
+
- **DataDive support:** support@datadive.tools
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
var DEFAULT_BASE_URL = "https://api.datadive.tools";
|
|
8
|
+
var KEY_HELP_URL = "https://app.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
|
+
return { apiKey, baseUrl };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// src/server.ts
|
|
22
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
23
|
+
|
|
24
|
+
// src/tools/list-niches.ts
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
|
|
27
|
+
// src/http/errors.ts
|
|
28
|
+
var KEY_HELP_URL2 = "https://app.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://app.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
|
+
// src/http/client.ts
|
|
103
|
+
var PKG_VERSION = "0.1.0";
|
|
104
|
+
var PAGINATION_KEYS = ["currentPage", "lastPage", "hasNext", "hasPrev", "pageSize", "total"];
|
|
105
|
+
async function httpGet(ctx, path, query) {
|
|
106
|
+
const url = buildUrl(ctx.config.baseUrl, path, query);
|
|
107
|
+
let res;
|
|
108
|
+
try {
|
|
109
|
+
res = await fetch(url, {
|
|
110
|
+
method: "GET",
|
|
111
|
+
headers: {
|
|
112
|
+
"x-api-key": ctx.config.apiKey,
|
|
113
|
+
accept: "application/json",
|
|
114
|
+
"user-agent": userAgent(ctx.toolName)
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
} catch (e) {
|
|
118
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
119
|
+
throw new ApiError("network", 0, `Network error reaching ${url}: ${msg}`);
|
|
120
|
+
}
|
|
121
|
+
const text = await res.text();
|
|
122
|
+
let body;
|
|
123
|
+
try {
|
|
124
|
+
body = text.length > 0 ? JSON.parse(text) : null;
|
|
125
|
+
} catch {
|
|
126
|
+
body = text;
|
|
127
|
+
}
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
throw ApiError.fromHttp(res.status, body);
|
|
130
|
+
}
|
|
131
|
+
return unwrap(body);
|
|
132
|
+
}
|
|
133
|
+
function buildUrl(baseUrl, path, query) {
|
|
134
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
135
|
+
const u = new URL(`${baseUrl}${normalizedPath}`);
|
|
136
|
+
if (query) {
|
|
137
|
+
for (const [k, v] of Object.entries(query)) {
|
|
138
|
+
if (v === void 0 || v === null) continue;
|
|
139
|
+
u.searchParams.set(k, String(v));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return u.toString();
|
|
143
|
+
}
|
|
144
|
+
function userAgent(toolName) {
|
|
145
|
+
return `datadive-mcp/${PKG_VERSION} tool/${toolName}`;
|
|
146
|
+
}
|
|
147
|
+
function unwrap(body) {
|
|
148
|
+
if (!isObject(body)) return body;
|
|
149
|
+
if (!("data" in body)) return body;
|
|
150
|
+
for (const key of PAGINATION_KEYS) {
|
|
151
|
+
if (key in body) return body;
|
|
152
|
+
}
|
|
153
|
+
return body.data;
|
|
154
|
+
}
|
|
155
|
+
function isObject(v) {
|
|
156
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/tools/list-niches.ts
|
|
160
|
+
var inputSchema = {
|
|
161
|
+
currentPage: z.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
|
|
162
|
+
pageSize: z.number().int().min(1).max(100).optional().describe("Items per page (max 100). Defaults to 20.")
|
|
163
|
+
};
|
|
164
|
+
var listNichesTool = {
|
|
165
|
+
name: "list_niches",
|
|
166
|
+
title: "List DataDive Niches",
|
|
167
|
+
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).",
|
|
168
|
+
inputSchema,
|
|
169
|
+
handler: async (args, ctx) => {
|
|
170
|
+
return await httpGet(
|
|
171
|
+
{ config: ctx.config, toolName: "list_niches" },
|
|
172
|
+
"/v1/niches",
|
|
173
|
+
{ currentPage: args.currentPage, pageSize: args.pageSize }
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/tools/get-niche-keywords.ts
|
|
179
|
+
import { z as z2 } from "zod";
|
|
180
|
+
var inputSchema2 = {
|
|
181
|
+
nicheId: z2.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
|
|
182
|
+
};
|
|
183
|
+
var getNicheKeywordsTool = {
|
|
184
|
+
name: "get_niche_keywords",
|
|
185
|
+
title: "Get Master Keyword List for a Niche",
|
|
186
|
+
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.',
|
|
187
|
+
inputSchema: inputSchema2,
|
|
188
|
+
handler: async (args, ctx) => {
|
|
189
|
+
return await httpGet(
|
|
190
|
+
{ config: ctx.config, toolName: "get_niche_keywords" },
|
|
191
|
+
`/v1/niches/${encodeURIComponent(args.nicheId)}/keywords`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// src/tools/get-niche-competitors.ts
|
|
197
|
+
import { z as z3 } from "zod";
|
|
198
|
+
var inputSchema3 = {
|
|
199
|
+
nicheId: z3.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
|
|
200
|
+
};
|
|
201
|
+
var getNicheCompetitorsTool = {
|
|
202
|
+
name: "get_niche_competitors",
|
|
203
|
+
title: "Get Competitors and Niche Statistics",
|
|
204
|
+
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.",
|
|
205
|
+
inputSchema: inputSchema3,
|
|
206
|
+
handler: async (args, ctx) => {
|
|
207
|
+
return await httpGet(
|
|
208
|
+
{ config: ctx.config, toolName: "get_niche_competitors" },
|
|
209
|
+
`/v1/niches/${encodeURIComponent(args.nicheId)}/competitors`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// src/tools/get-ranking-juice.ts
|
|
215
|
+
import { z as z4 } from "zod";
|
|
216
|
+
var inputSchema4 = {
|
|
217
|
+
nicheId: z4.string().min(1).describe("The unique identifier of the Niche (from `list_niches`).")
|
|
218
|
+
};
|
|
219
|
+
var getRankingJuiceTool = {
|
|
220
|
+
name: "get_ranking_juice",
|
|
221
|
+
title: "Get Ranking Juices for a Niche",
|
|
222
|
+
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.",
|
|
223
|
+
inputSchema: inputSchema4,
|
|
224
|
+
handler: async (args, ctx) => {
|
|
225
|
+
return await httpGet(
|
|
226
|
+
{ config: ctx.config, toolName: "get_ranking_juice" },
|
|
227
|
+
`/v1/niches/${encodeURIComponent(args.nicheId)}/ranking-juices`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/tools/list-rank-radars.ts
|
|
233
|
+
import { z as z5 } from "zod";
|
|
234
|
+
|
|
235
|
+
// src/types/api.ts
|
|
236
|
+
var RANK_RADAR_STATUSES = ["ACTIVE", "PAUSED", "ARCHIVED"];
|
|
237
|
+
|
|
238
|
+
// src/tools/list-rank-radars.ts
|
|
239
|
+
var inputSchema5 = {
|
|
240
|
+
currentPage: z5.number().int().min(1).optional().describe("Page number, 1-indexed. Defaults to 1."),
|
|
241
|
+
pageSize: z5.number().int().min(1).max(100).optional().describe("Items per page (max 100). Defaults to 20."),
|
|
242
|
+
nicheId: z5.string().min(1).optional().describe("Filter Rank Radars by Niche identifier. Use after `list_niches`."),
|
|
243
|
+
status: z5.enum(RANK_RADAR_STATUSES).optional().describe(
|
|
244
|
+
"Filter by Rank Radar status. By default, returns only ACTIVE Rank Radars. Allowed: ACTIVE, PAUSED, ARCHIVED."
|
|
245
|
+
),
|
|
246
|
+
searchText: z5.string().optional().describe("Filter Rank Radars by ASIN or product title.")
|
|
247
|
+
};
|
|
248
|
+
var listRankRadarsTool = {
|
|
249
|
+
name: "list_rank_radars",
|
|
250
|
+
title: "List Rank Radars",
|
|
251
|
+
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).",
|
|
252
|
+
inputSchema: inputSchema5,
|
|
253
|
+
handler: async (args, ctx) => {
|
|
254
|
+
return await httpGet(
|
|
255
|
+
{ config: ctx.config, toolName: "list_rank_radars" },
|
|
256
|
+
"/v1/niches/rank-radars",
|
|
257
|
+
{
|
|
258
|
+
currentPage: args.currentPage,
|
|
259
|
+
pageSize: args.pageSize,
|
|
260
|
+
nicheId: args.nicheId,
|
|
261
|
+
status: args.status,
|
|
262
|
+
searchText: args.searchText
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
// src/tools/get-rank-radar-data.ts
|
|
269
|
+
import { z as z6 } from "zod";
|
|
270
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
271
|
+
var inputSchema6 = {
|
|
272
|
+
rankRadarId: z6.string().uuid().describe("The Rank Radar UUID (from `list_rank_radars`)."),
|
|
273
|
+
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)."),
|
|
274
|
+
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.")
|
|
275
|
+
};
|
|
276
|
+
var getRankRadarDataTool = {
|
|
277
|
+
name: "get_rank_radar_data",
|
|
278
|
+
title: "Get Keyword Rankings for a Rank Radar",
|
|
279
|
+
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`.",
|
|
280
|
+
inputSchema: inputSchema6,
|
|
281
|
+
handler: async (args, ctx) => {
|
|
282
|
+
return await httpGet(
|
|
283
|
+
{ config: ctx.config, toolName: "get_rank_radar_data" },
|
|
284
|
+
`/v1/niches/rank-radars/${encodeURIComponent(args.rankRadarId)}`,
|
|
285
|
+
{ startDate: args.startDate, endDate: args.endDate }
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// src/tools/index.ts
|
|
291
|
+
var allTools = [
|
|
292
|
+
listNichesTool,
|
|
293
|
+
getNicheKeywordsTool,
|
|
294
|
+
getNicheCompetitorsTool,
|
|
295
|
+
getRankingJuiceTool,
|
|
296
|
+
listRankRadarsTool,
|
|
297
|
+
getRankRadarDataTool
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
// src/server.ts
|
|
301
|
+
function buildServer(config) {
|
|
302
|
+
const server = new McpServer({
|
|
303
|
+
name: "datadive",
|
|
304
|
+
version: PKG_VERSION
|
|
305
|
+
});
|
|
306
|
+
for (const tool of allTools) {
|
|
307
|
+
server.registerTool(
|
|
308
|
+
tool.name,
|
|
309
|
+
{
|
|
310
|
+
title: tool.title,
|
|
311
|
+
description: tool.description,
|
|
312
|
+
inputSchema: tool.inputSchema
|
|
313
|
+
},
|
|
314
|
+
async (args) => {
|
|
315
|
+
try {
|
|
316
|
+
const data = await tool.handler(args, { config });
|
|
317
|
+
return {
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: JSON.stringify(data, null, 2)
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
};
|
|
325
|
+
} catch (err) {
|
|
326
|
+
return {
|
|
327
|
+
isError: true,
|
|
328
|
+
content: [
|
|
329
|
+
{
|
|
330
|
+
type: "text",
|
|
331
|
+
text: err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err)
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return server;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/index.ts
|
|
343
|
+
async function main() {
|
|
344
|
+
let config;
|
|
345
|
+
try {
|
|
346
|
+
config = loadConfig();
|
|
347
|
+
} catch (err) {
|
|
348
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
349
|
+
process.stderr.write(`[datadive-mcp] ${msg}
|
|
350
|
+
`);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
const server = buildServer(config);
|
|
354
|
+
const transport = new StdioServerTransport();
|
|
355
|
+
await server.connect(transport);
|
|
356
|
+
}
|
|
357
|
+
main().catch((err) => {
|
|
358
|
+
const msg = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
359
|
+
process.stderr.write(`[datadive-mcp] fatal: ${msg}
|
|
360
|
+
`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|
|
363
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/server.ts","../src/tools/list-niches.ts","../src/http/errors.ts","../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/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 app.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://app.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 } 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 return {\n content: [\n {\n type: \"text\" as const,\n text: JSON.stringify(data, null, 2),\n },\n ],\n };\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://app.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://app.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 * 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\";\n\n/** Bumped at release time; written into the User-Agent header. */\nexport const PKG_VERSION = \"0.1.0\";\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 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","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","/**\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 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] 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"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@datadive-tools/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for DataDive — query your niches, keywords, competitors, and Rank Radar data from Claude Desktop, Claude Code, or Cursor.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"datadive-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.17"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"dev": "tsup --watch",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"test:smoke": "DATADIVE_SMOKE=1 vitest run test/smoke.test.ts",
|
|
24
|
+
"lint": "eslint .",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"mcp",
|
|
30
|
+
"model-context-protocol",
|
|
31
|
+
"datadive",
|
|
32
|
+
"amazon",
|
|
33
|
+
"claude"
|
|
34
|
+
],
|
|
35
|
+
"author": "DataDive",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"homepage": "https://github.com/Data-Dive-Tools/datadive-mcp",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/Data-Dive-Tools/datadive-mcp.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/Data-Dive-Tools/datadive-mcp/issues"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
47
|
+
"zod": "^3.23.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@changesets/cli": "^2.27.0",
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
53
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
54
|
+
"eslint": "^9.0.0",
|
|
55
|
+
"prettier": "^3.0.0",
|
|
56
|
+
"tsup": "^8.0.0",
|
|
57
|
+
"typescript": "~5.9.0",
|
|
58
|
+
"vitest": "^2.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|