@0pen1/scrapingdog-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 +152 -0
- package/dist/index.js +39 -0
- package/dist/result.js +43 -0
- package/dist/scrapingdog.js +142 -0
- package/dist/tools.js +415 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 0pen1
|
|
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,152 @@
|
|
|
1
|
+
# scrapingdog-mcp
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
|
|
4
|
+
[Scrapingdog](https://www.scrapingdog.com/documentation/) scraping APIs as tools for
|
|
5
|
+
MCP-compatible clients (Claude Desktop, Claude Code, and others).
|
|
6
|
+
|
|
7
|
+
Communicates over **stdio**. Built with TypeScript and the official MCP SDK.
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
Nine tools, each wrapping one Scrapingdog endpoint:
|
|
12
|
+
|
|
13
|
+
| Tool | Endpoint | What it returns |
|
|
14
|
+
|------|----------|-----------------|
|
|
15
|
+
| `scrape` | `/scrape` | Raw HTML of any URL (JS rendering, premium proxies, geotargeting, sessions, stealth, wait) |
|
|
16
|
+
| `screenshot` | `/screenshot` | Page screenshot metadata (png/jpg/webp, full-page, viewport, quality) |
|
|
17
|
+
| `google_search` | `/google` | Google organic results, ads, knowledge graph, SERP features (JSON or HTML) |
|
|
18
|
+
| `bing_search` | `/bing/search` | Bing search results (market/geo/pagination/safe-search) |
|
|
19
|
+
| `duckduckgo_search` | `/duckduckgo/search` | DuckDuckGo results (region, date filter, pagination token) |
|
|
20
|
+
| `baidu_search` | `/baidu/search` | Baidu results (Chinese-language restriction, pagination) |
|
|
21
|
+
| `x_profile` | `/x/profile` | X (Twitter) profile data (name, handle, follower counts, bio) |
|
|
22
|
+
| `x_post` | `/x/post` | X (Twitter) post (tweet) data |
|
|
23
|
+
| `datacenter_proxy` | *(forward proxy)* | Connection details + ready-to-paste curl/Python for `proxy.scrapingdog.com:8081` |
|
|
24
|
+
|
|
25
|
+
Every tool accepts an optional `api_key` argument to override the configured key for that call.
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
- Node.js **≥ 18.17** (tested on 20.x).
|
|
30
|
+
- A Scrapingdog API key — get one from your [Scrapingdog dashboard](https://www.scrapingdog.com/) after signing up.
|
|
31
|
+
|
|
32
|
+
## Install & build
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
npm run build # outputs to dist/
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Provide your API key
|
|
40
|
+
|
|
41
|
+
The key is resolved in this order (first wins):
|
|
42
|
+
|
|
43
|
+
1. The `api_key` argument on an individual tool call.
|
|
44
|
+
2. The `SCRAPINGDOG_API_KEY` environment variable.
|
|
45
|
+
3. A `.env` file containing `SCRAPINGDOG_API_KEY=...` in the working directory,
|
|
46
|
+
`~/.scrapingdog.env`, or `~/.env`.
|
|
47
|
+
|
|
48
|
+
Option **2** (env var) is recommended — the key never touches disk beyond your MCP
|
|
49
|
+
client config, and nothing ends up in tool-call logs.
|
|
50
|
+
|
|
51
|
+
## Configure your MCP client
|
|
52
|
+
|
|
53
|
+
### Claude Desktop / Claude Code (`claude_desktop_config.json`)
|
|
54
|
+
|
|
55
|
+
```jsonc
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"scrapingdog": {
|
|
59
|
+
"command": "node",
|
|
60
|
+
"args": ["/absolute/path/to/scrcpy/dist/index.js"],
|
|
61
|
+
"env": {
|
|
62
|
+
"SCRAPINGDOG_API_KEY": "your-key-here"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Install from npm (no clone needed)
|
|
70
|
+
|
|
71
|
+
Published as [`@0pen1/scrapingdog-mcp`](https://www.npmjs.com/package/@0pen1/scrapingdog-mcp). Run directly with `npx`:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx @0pen1/scrapingdog-mcp
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Add it to Claude Code in one line (the server reads `SCRAPINGDOG_API_KEY` from the env you pass):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
claude mcp add scrapingdog --scope user \
|
|
81
|
+
--env SCRAPINGDOG_API_KEY=your-key-here \
|
|
82
|
+
-- npx -y @0pen1/scrapingdog-mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Or in an MCP client config (`claude_desktop_config.json` etc.):
|
|
86
|
+
|
|
87
|
+
```jsonc
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"scrapingdog": {
|
|
91
|
+
"command": "npx",
|
|
92
|
+
"args": ["-y", "@0pen1/scrapingdog-mcp"],
|
|
93
|
+
"env": { "SCRAPINGDOG_API_KEY": "your-key-here" }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Run standalone (for testing)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
SCRAPINGDOG_API_KEY=your-key node dist/index.js
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
It speaks JSON-RPC over stdio, so you can pipe messages to it directly.
|
|
106
|
+
|
|
107
|
+
## Example tool calls
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
scrape { "url": "https://example.com", "dynamic": true, "country": "us" }
|
|
111
|
+
google_search { "query": "best espresso machines 2026", "results": "10", "country": "us" }
|
|
112
|
+
bing_search { "query": "site:github.com mcp server", "count": "20" }
|
|
113
|
+
duckduckgo_search { "query": "rust async runtime", "df": "m" }
|
|
114
|
+
baidu_search { "query": "人工智能", "ct": 2 }
|
|
115
|
+
x_profile { "profileId": "elonmusk" }
|
|
116
|
+
x_post { "tweetId": "1655608985058267139" }
|
|
117
|
+
screenshot { "url": "https://example.com", "fullPage": true, "format": "png" }
|
|
118
|
+
datacenter_proxy { "target_url": "https://httpbin.org/ip" }
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## How responses are handled
|
|
122
|
+
|
|
123
|
+
- **JSON endpoints** (search, social, etc.): the parsed JSON is returned as text so the
|
|
124
|
+
host can reason about it directly.
|
|
125
|
+
- **`/scrape`**: raw HTML is returned as text.
|
|
126
|
+
- **`/screenshot`**: the endpoint returns binary image bytes, which can't be carried as
|
|
127
|
+
text cleanly. The tool reports the content type and byte length, and notes how to fetch
|
|
128
|
+
the image directly. (If you need true image delivery, extend the handler to base64-encode
|
|
129
|
+
into an `image` content block.)
|
|
130
|
+
- **Non-2xx responses**: returned as MCP error results (`isError: true`) with the upstream
|
|
131
|
+
status, content type, and body. Note Scrapingdog reports invalid keys as HTTP 400 with a
|
|
132
|
+
JSON "Unauthorized request" message — the body makes this clear.
|
|
133
|
+
|
|
134
|
+
## Credit costs (per request)
|
|
135
|
+
|
|
136
|
+
Scrape: 1 (rotating proxy) → 25 (JS render + premium). Google/Bing/DDG/Baidu search: ~5.
|
|
137
|
+
Screenshot: 5. Profile/Post scrapers: 5–10. See the [pricing/credit table](https://www.scrapingdog.com/documentation/)
|
|
138
|
+
for the full breakdown. Requests time out after 60s upstream.
|
|
139
|
+
|
|
140
|
+
## Project layout
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
src/
|
|
144
|
+
index.ts # entry point — creates McpServer, connects stdio transport
|
|
145
|
+
scrapingdog.ts # API key resolution + shared HTTP request helper
|
|
146
|
+
result.ts # response → MCP tool-result formatting (ok / error / fromResponse)
|
|
147
|
+
tools.ts # the 9 tool definitions (schema + handler) and registration
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scrapingdog MCP Server — entry point.
|
|
4
|
+
*
|
|
5
|
+
* A Model Context Protocol server that exposes the Scrapingdog scraping APIs
|
|
6
|
+
* as tools for MCP-compatible clients (Claude Desktop, Claude Code, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Communicates over stdio. The API key is resolved from (in order):
|
|
9
|
+
* 1. the `api_key` argument on each tool call,
|
|
10
|
+
* 2. the SCRAPINGDOG_API_KEY environment variable,
|
|
11
|
+
* 3. a .env file in the cwd / home directory.
|
|
12
|
+
*
|
|
13
|
+
* Run directly: node dist/index.js
|
|
14
|
+
* Or after build: scrapingdog-mcp
|
|
15
|
+
*/
|
|
16
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { registerTools } from "./tools.js";
|
|
19
|
+
async function main() {
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: "scrapingdog-mcp",
|
|
22
|
+
version: "1.0.0",
|
|
23
|
+
}, {
|
|
24
|
+
instructions: "Scrapingdog MCP server. Tools wrap the Scrapingdog web-scraping and search APIs: " +
|
|
25
|
+
"scrape (any URL), screenshot, google_search, bing_search, duckduckgo_search, baidu_search, " +
|
|
26
|
+
"x_profile, x_post, and datacenter_proxy. " +
|
|
27
|
+
"The API key is read from SCRAPINGDOG_API_KEY, a .env file, or per-call api_key argument.",
|
|
28
|
+
});
|
|
29
|
+
registerTools(server);
|
|
30
|
+
const transport = new StdioServerTransport();
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
// Keep the process alive; the transport owns stdin/stdout. Logging to stderr
|
|
33
|
+
// so it never corrupts the stdio JSON-RPC stream on stdout.
|
|
34
|
+
process.stderr.write("[scrapingdog-mcp] server running on stdio\n");
|
|
35
|
+
}
|
|
36
|
+
main().catch((err) => {
|
|
37
|
+
process.stderr.write(`[scrapingdog-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
package/dist/result.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for turning a Scrapingdog {@link SdResponse} into an MCP tool result.
|
|
3
|
+
*
|
|
4
|
+
* Scrapingdog endpoints return a mix of JSON, raw HTML, and binary images.
|
|
5
|
+
* The server surfaces whatever comes back so the host (the LLM) can decide
|
|
6
|
+
* what to do with it, while flagging HTTP errors explicitly.
|
|
7
|
+
*/
|
|
8
|
+
/** Build a success result from a Scrapingdog response. */
|
|
9
|
+
export function ok(result, opts) {
|
|
10
|
+
const parts = [];
|
|
11
|
+
if (opts?.note)
|
|
12
|
+
parts.push(opts.note);
|
|
13
|
+
// Prefer JSON when we have it — easier for the host to reason about.
|
|
14
|
+
if (result.json !== null) {
|
|
15
|
+
parts.push(JSON.stringify(result.json, null, 2));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
parts.push(result.body);
|
|
19
|
+
}
|
|
20
|
+
return { content: [{ type: "text", text: parts.join("\n\n") }] };
|
|
21
|
+
}
|
|
22
|
+
/** Build an error result, surfacing the upstream status + body when present. */
|
|
23
|
+
export function error(message, result) {
|
|
24
|
+
const parts = [message];
|
|
25
|
+
if (result) {
|
|
26
|
+
parts.push(`HTTP ${result.status} (${result.contentType})`);
|
|
27
|
+
parts.push(result.body || "(empty body)");
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: parts.join("\n\n") }],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Decide success vs. failure by status code. Non-2xx → error result.
|
|
36
|
+
* 202 (queued) is treated as success since Scrapingdog uses it for accepted jobs.
|
|
37
|
+
*/
|
|
38
|
+
export function fromResponse(result, opts) {
|
|
39
|
+
if (result.status >= 200 && result.status < 300) {
|
|
40
|
+
return ok(result, opts);
|
|
41
|
+
}
|
|
42
|
+
return error(`Scrapingdog returned an error.`, result);
|
|
43
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scrapingdog API client + configuration.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the API key with this precedence:
|
|
5
|
+
* 1. The `api_key` argument passed to an individual tool call (highest).
|
|
6
|
+
* 2. The `SCRAPINGDOG_API_KEY` environment variable.
|
|
7
|
+
* 3. A `.env` file in the working directory (SCRAPINGDOG_API_KEY=...).
|
|
8
|
+
*
|
|
9
|
+
* All requests go to https://api.scrapingdog.com. The shared request helper
|
|
10
|
+
* injects the resolved key, forwards every caller-provided parameter, and
|
|
11
|
+
* transparently returns text or JSON depending on the response content type.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
export const API_BASE = "https://api.scrapingdog.com";
|
|
17
|
+
export const DEFAULT_TIMEOUT_MS = 90_000; // Scrapingdog times out at 60s; leave headroom for network.
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the API key. Caller-supplied key wins, then env, then .env file.
|
|
20
|
+
* Throws if no key can be found.
|
|
21
|
+
*/
|
|
22
|
+
export async function resolveApiKey(explicit) {
|
|
23
|
+
if (explicit && explicit.trim()) {
|
|
24
|
+
return explicit.trim();
|
|
25
|
+
}
|
|
26
|
+
const envKey = process.env.SCRAPINGDOG_API_KEY;
|
|
27
|
+
if (envKey && envKey.trim()) {
|
|
28
|
+
return envKey.trim();
|
|
29
|
+
}
|
|
30
|
+
// Fall back to a .env file in the cwd or the user's home directory.
|
|
31
|
+
const fromFile = await readDotenvKey();
|
|
32
|
+
if (fromFile) {
|
|
33
|
+
return fromFile;
|
|
34
|
+
}
|
|
35
|
+
throw new Error("No Scrapingdog API key found. Set SCRAPINGDOG_API_KEY in the environment, " +
|
|
36
|
+
"put it in a .env file (SCRAPINGDOG_API_KEY=...), or pass api_key to the tool call.");
|
|
37
|
+
}
|
|
38
|
+
/** Read SCRAPINGDOG_API_KEY from a .env file (cwd first, then home dir). */
|
|
39
|
+
async function readDotenvKey() {
|
|
40
|
+
const candidates = [
|
|
41
|
+
join(process.cwd(), ".env"),
|
|
42
|
+
join(homedir(), ".scrapingdog.env"),
|
|
43
|
+
join(homedir(), ".env"),
|
|
44
|
+
];
|
|
45
|
+
for (const path of candidates) {
|
|
46
|
+
try {
|
|
47
|
+
const contents = await readFile(path, "utf8");
|
|
48
|
+
const key = parseDotenv(contents);
|
|
49
|
+
if (key)
|
|
50
|
+
return key;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// File missing or unreadable — try the next candidate.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Minimal .env parser: finds SCRAPINGDOG_API_KEY=..., strips quotes/comments. */
|
|
59
|
+
function parseDotenv(contents) {
|
|
60
|
+
for (const rawLine of contents.split("\n")) {
|
|
61
|
+
const line = rawLine.trim();
|
|
62
|
+
if (!line || line.startsWith("#"))
|
|
63
|
+
continue;
|
|
64
|
+
const eq = line.indexOf("=");
|
|
65
|
+
if (eq === -1)
|
|
66
|
+
continue;
|
|
67
|
+
const name = line.slice(0, eq).trim();
|
|
68
|
+
if (name !== "SCRAPINGDOG_API_KEY")
|
|
69
|
+
continue;
|
|
70
|
+
let value = line.slice(eq + 1).trim();
|
|
71
|
+
// Strip inline comment.
|
|
72
|
+
const hash = value.indexOf(" #");
|
|
73
|
+
if (hash !== -1)
|
|
74
|
+
value = value.slice(0, hash).trim();
|
|
75
|
+
// Strip surrounding quotes.
|
|
76
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
77
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
78
|
+
value = value.slice(1, -1);
|
|
79
|
+
}
|
|
80
|
+
if (value)
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Send a request to the Scrapingdog API and return a normalized result.
|
|
87
|
+
*
|
|
88
|
+
* Booleans in `params` are lowercased (`true`/`false`) to match the API's
|
|
89
|
+
* expected string form. `undefined` values are dropped. Numbers are stringified.
|
|
90
|
+
*/
|
|
91
|
+
export async function sdRequest(opts) {
|
|
92
|
+
const apiKey = await resolveApiKey(opts.params.api_key);
|
|
93
|
+
const params = new URLSearchParams();
|
|
94
|
+
params.set("api_key", apiKey);
|
|
95
|
+
for (const [key, value] of Object.entries(opts.params)) {
|
|
96
|
+
if (key === "api_key")
|
|
97
|
+
continue; // already set, resolved form takes precedence
|
|
98
|
+
if (value === undefined || value === null)
|
|
99
|
+
continue;
|
|
100
|
+
if (typeof value === "boolean") {
|
|
101
|
+
params.set(key, value ? "true" : "false");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
params.set(key, String(value));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const url = `${API_BASE}/${opts.path.replace(/^\/+/, "")}?${params.toString()}`;
|
|
108
|
+
const controller = new AbortController();
|
|
109
|
+
const timeout = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
110
|
+
let response;
|
|
111
|
+
try {
|
|
112
|
+
response = await fetch(url, {
|
|
113
|
+
method: opts.method ?? "GET",
|
|
114
|
+
headers: opts.headers,
|
|
115
|
+
body: opts.body,
|
|
116
|
+
signal: controller.signal,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
clearTimeout(timeout);
|
|
121
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
122
|
+
throw new Error(`Scrapingdog request failed: ${message}`);
|
|
123
|
+
}
|
|
124
|
+
clearTimeout(timeout);
|
|
125
|
+
const body = await response.text();
|
|
126
|
+
const contentType = response.headers.get("content-type") ?? "text/plain";
|
|
127
|
+
let json = null;
|
|
128
|
+
if (contentType.includes("application/json")) {
|
|
129
|
+
try {
|
|
130
|
+
json = JSON.parse(body);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Body wasn't valid JSON even though the header said so — leave json null.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
status: response.status,
|
|
138
|
+
body,
|
|
139
|
+
contentType,
|
|
140
|
+
json,
|
|
141
|
+
};
|
|
142
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool definitions for the Scrapingdog MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Each tool wraps one Scrapingdog API endpoint documented at
|
|
5
|
+
* https://www.scrapingdog.com/documentation/. Parameters mirror the upstream
|
|
6
|
+
* query parameters; boolean flags are sent as lowercased "true"/"false".
|
|
7
|
+
*
|
|
8
|
+
* The API key is resolved per-call (see scrapingdog.ts); callers may override
|
|
9
|
+
* it by passing `api_key`.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
import { sdRequest } from "./scrapingdog.js";
|
|
13
|
+
import { fromResponse, error } from "./result.js";
|
|
14
|
+
/** Cast the loosely-typed SDK args into the stricter request-param map. */
|
|
15
|
+
function toParams(args) {
|
|
16
|
+
return args;
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Shared schema fragments
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** Common optional override for the API key. */
|
|
22
|
+
const apiKeyParam = {
|
|
23
|
+
api_key: z.string().optional().describe("Override the configured Scrapingdog API key for this call. If omitted, the key is read from SCRAPINGDOG_API_KEY or a local .env file."),
|
|
24
|
+
};
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// 1. Web Scraping API — /scrape
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const scrapeSchema = {
|
|
29
|
+
url: z.string().url().describe("The URL of the page to scrape."),
|
|
30
|
+
dynamic: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Enable JavaScript rendering with a headless browser. Costs 5 credits (25 with premium proxy)."),
|
|
34
|
+
premium: z
|
|
35
|
+
.boolean()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Use premium residential proxies instead of the rotating datacenter proxy. Costs 10 credits (25 with JS rendering)."),
|
|
38
|
+
country: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("ISO country code for geotargeting (e.g. 'us', 'gb', 'in'). Sends the request from that location. Costs 10 credits."),
|
|
42
|
+
session_number: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Reuse the same proxy/IP across multiple requests by passing a stable session string."),
|
|
46
|
+
custom_headers: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Set to true to forward your own request headers (passed via the headers parameter)."),
|
|
50
|
+
wait: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.min(0)
|
|
54
|
+
.max(35000)
|
|
55
|
+
.optional()
|
|
56
|
+
.describe("Milliseconds to wait after JS rendering before capturing HTML (0–35000). Use with dynamic=true."),
|
|
57
|
+
stealth_mode: z
|
|
58
|
+
.boolean()
|
|
59
|
+
.optional()
|
|
60
|
+
.describe("Enable stealth mode to bypass bot detection / captchas."),
|
|
61
|
+
image: z.boolean().optional().describe("Scrape image URLs found on the page."),
|
|
62
|
+
...apiKeyParam,
|
|
63
|
+
};
|
|
64
|
+
async function handleScrape(args) {
|
|
65
|
+
const result = await sdRequest({ path: "scrape", params: toParams(args) });
|
|
66
|
+
return fromResponse(result, { note: `Scraped HTML from ${args.url}.` });
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// 2. Screenshot API — /screenshot
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
const screenshotSchema = {
|
|
72
|
+
url: z.string().url().describe("The URL of the page to screenshot."),
|
|
73
|
+
fullPage: z
|
|
74
|
+
.boolean()
|
|
75
|
+
.optional()
|
|
76
|
+
.describe("Capture the full scrollable page rather than just the visible viewport."),
|
|
77
|
+
width: z
|
|
78
|
+
.string()
|
|
79
|
+
.optional()
|
|
80
|
+
.describe("Browser viewport width in pixels (e.g. '1920')."),
|
|
81
|
+
height: z.string().optional().describe("Browser viewport height in pixels (e.g. '1080')."),
|
|
82
|
+
wait_until: z
|
|
83
|
+
.enum(["load", "domcontentloaded", "networkidle"])
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("When navigation is considered complete before screenshotting. Default: domcontentloaded."),
|
|
86
|
+
format: z
|
|
87
|
+
.enum(["png", "jpg", "webp"])
|
|
88
|
+
.optional()
|
|
89
|
+
.describe("Output image format. Default: png."),
|
|
90
|
+
quality: z
|
|
91
|
+
.number()
|
|
92
|
+
.int()
|
|
93
|
+
.min(0)
|
|
94
|
+
.max(100)
|
|
95
|
+
.optional()
|
|
96
|
+
.describe("Image quality 0–100 (jpg/webp). Default: 80."),
|
|
97
|
+
...apiKeyParam,
|
|
98
|
+
};
|
|
99
|
+
async function handleScreenshot(args) {
|
|
100
|
+
const result = await sdRequest({ path: "screenshot", params: toParams(args) });
|
|
101
|
+
if (result.status >= 200 && result.status < 300) {
|
|
102
|
+
// The screenshot endpoint returns binary image bytes. We can't return raw
|
|
103
|
+
// binary as text cleanly via this tool, so we describe what happened and
|
|
104
|
+
// report the content type / length. For a true image, a host would base64
|
|
105
|
+
// it; here we surface status + size and the first bytes if it's text.
|
|
106
|
+
if (result.contentType.startsWith("image/")) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: `Screenshot captured (${result.contentType}, ${result.body.length} bytes). The image bytes were returned by Scrapingdog but are binary; this MCP tool returns text. To save the image, call the Scrapingdog /screenshot endpoint directly and write the response body to a file.`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return fromResponse(result, { note: `Screenshot response for ${args.url}.` });
|
|
117
|
+
}
|
|
118
|
+
return error("Scrapingdog screenshot failed.", result);
|
|
119
|
+
}
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// 3. Google Search API — /google
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
const googleSearchSchema = {
|
|
124
|
+
query: z.string().describe("The Google search query. Supports operators like site:, inurl:, intitle:."),
|
|
125
|
+
results: z
|
|
126
|
+
.string()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe("Number of results per page."),
|
|
129
|
+
page: z
|
|
130
|
+
.string()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("Page number: 0 = first page, 1 = second, etc. Default: 0."),
|
|
133
|
+
advance_search: z
|
|
134
|
+
.boolean()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Get advanced feature snippets. true costs 10 credits, otherwise 5."),
|
|
137
|
+
mob_search: z
|
|
138
|
+
.boolean()
|
|
139
|
+
.optional()
|
|
140
|
+
.describe("Return mobile search results. true costs 10 credits."),
|
|
141
|
+
html: z.boolean().optional().describe("Return the full raw HTML of the Google results page."),
|
|
142
|
+
domain: z
|
|
143
|
+
.string()
|
|
144
|
+
.optional()
|
|
145
|
+
.describe("Google domain for localized results, e.g. google.co.in, google.co.uk. Default: google.com."),
|
|
146
|
+
country: z.string().optional().describe("Two-letter country code, e.g. 'us', 'uk', 'fr'. Default: us."),
|
|
147
|
+
language: z.string().optional().describe("Result language code, e.g. 'en', 'es', 'fr'. Default: en."),
|
|
148
|
+
location: z
|
|
149
|
+
.string()
|
|
150
|
+
.optional()
|
|
151
|
+
.describe("Origin location (city-level recommended). Cannot be used with uule."),
|
|
152
|
+
uule: z
|
|
153
|
+
.string()
|
|
154
|
+
.optional()
|
|
155
|
+
.describe("Encoded geographic locale for tailored results. Cannot be combined with location."),
|
|
156
|
+
tbs: z
|
|
157
|
+
.string()
|
|
158
|
+
.optional()
|
|
159
|
+
.describe("Advanced filter string (time ranges, verbatim mode) copied from Google's URL."),
|
|
160
|
+
safe: z
|
|
161
|
+
.enum(["active", "off"])
|
|
162
|
+
.optional()
|
|
163
|
+
.describe("Filter adult content: 'active' to filter, 'off' to disable. Default: off."),
|
|
164
|
+
...apiKeyParam,
|
|
165
|
+
};
|
|
166
|
+
async function handleGoogleSearch(args) {
|
|
167
|
+
const result = await sdRequest({ path: "google", params: toParams(args) });
|
|
168
|
+
return fromResponse(result, { note: `Google search results for "${args.query}".` });
|
|
169
|
+
}
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// 4. Bing Search API — /bing/search
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
const bingSearchSchema = {
|
|
174
|
+
query: z.string().describe("The Bing search query. Supports NOT, OR, site:, filetype:, near:, etc."),
|
|
175
|
+
cc: z.string().optional().describe("Two-letter ISO country code, e.g. 'us', 'de', 'gb'."),
|
|
176
|
+
mkt: z
|
|
177
|
+
.string()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Market as <language>-<country>, e.g. 'en-US'. Mutually exclusive with cc."),
|
|
180
|
+
lat: z.string().optional().describe("GPS latitude for the search starting point."),
|
|
181
|
+
lon: z.string().optional().describe("GPS longitude for the search starting point."),
|
|
182
|
+
first: z
|
|
183
|
+
.string()
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Starting position of organic results. Default: 1. first=10 puts the 10th result first."),
|
|
186
|
+
count: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Results per page, 1–50 (actual may vary)."),
|
|
190
|
+
safeSearch: z
|
|
191
|
+
.enum(["Off", "Moderate", "Strict"])
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Adult-content filter level."),
|
|
194
|
+
filters: z
|
|
195
|
+
.string()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Advanced filter string copied from a Bing search URL."),
|
|
198
|
+
...apiKeyParam,
|
|
199
|
+
};
|
|
200
|
+
async function handleBingSearch(args) {
|
|
201
|
+
const result = await sdRequest({ path: "bing/search", params: toParams(args) });
|
|
202
|
+
return fromResponse(result, { note: `Bing search results for "${args.query}".` });
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// 5. DuckDuckGo Search API — /duckduckgo/search
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
const duckduckgoSearchSchema = {
|
|
208
|
+
query: z.string().describe("The DuckDuckGo search query. Supports inurl:, site:, intitle: operators."),
|
|
209
|
+
kl: z
|
|
210
|
+
.string()
|
|
211
|
+
.optional()
|
|
212
|
+
.describe("Region, e.g. 'us-en', 'uk-en', 'fr-fr'."),
|
|
213
|
+
df: z
|
|
214
|
+
.string()
|
|
215
|
+
.optional()
|
|
216
|
+
.describe("Date filter: d (past day), w (week), m (month), y (year), or from_date..to_date."),
|
|
217
|
+
next_page_token: z
|
|
218
|
+
.string()
|
|
219
|
+
.optional()
|
|
220
|
+
.describe("Token from a previous response to fetch the next page (15 results per page)."),
|
|
221
|
+
html: z.boolean().optional().describe("Return the full raw HTML of the DuckDuckGo results page."),
|
|
222
|
+
...apiKeyParam,
|
|
223
|
+
};
|
|
224
|
+
async function handleDuckDuckGoSearch(args) {
|
|
225
|
+
const result = await sdRequest({ path: "duckduckgo/search", params: toParams(args) });
|
|
226
|
+
return fromResponse(result, { note: `DuckDuckGo search results for "${args.query}".` });
|
|
227
|
+
}
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// 6. Baidu Search API — /baidu/search
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
const baiduSearchSchema = {
|
|
232
|
+
query: z.string().describe("The Baidu search query. Supports inurl:, site:, intitle: operators."),
|
|
233
|
+
ct: z
|
|
234
|
+
.number()
|
|
235
|
+
.int()
|
|
236
|
+
.optional()
|
|
237
|
+
.describe("Language restriction: 1 = all, 2 = Simplified Chinese, 3 = Traditional Chinese."),
|
|
238
|
+
pn: z
|
|
239
|
+
.string()
|
|
240
|
+
.optional()
|
|
241
|
+
.describe("Result offset for pagination. 0 = first page, 10 = second, 20 = third, etc."),
|
|
242
|
+
rn: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("Maximum number of results to return."),
|
|
246
|
+
html: z.boolean().optional().describe("Return the full raw HTML of the Baidu results page."),
|
|
247
|
+
...apiKeyParam,
|
|
248
|
+
};
|
|
249
|
+
async function handleBaiduSearch(args) {
|
|
250
|
+
const result = await sdRequest({ path: "baidu/search", params: toParams(args) });
|
|
251
|
+
return fromResponse(result, { note: `Baidu search results for "${args.query}".` });
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// 7. X (Twitter) Profile Scraper — /x/profile
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
const xProfileSchema = {
|
|
257
|
+
profileId: z
|
|
258
|
+
.string()
|
|
259
|
+
.describe("The user ID or username of the X profile to scrape, e.g. 'elonmusk' or 'nasa'."),
|
|
260
|
+
...apiKeyParam,
|
|
261
|
+
};
|
|
262
|
+
async function handleXProfile(args) {
|
|
263
|
+
const result = await sdRequest({ path: "x/profile", params: toParams(args) });
|
|
264
|
+
return fromResponse(result, { note: `X (Twitter) profile data for "${args.profileId}".` });
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
// 8. X (Twitter) Post Scraper — /x/post
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
const xPostSchema = {
|
|
270
|
+
tweetId: z
|
|
271
|
+
.string()
|
|
272
|
+
.describe("The tweet ID, found in the post URL (e.g. for .../status/1655608985058267139 the ID is 1655608985058267139)."),
|
|
273
|
+
...apiKeyParam,
|
|
274
|
+
};
|
|
275
|
+
async function handleXPost(args) {
|
|
276
|
+
const result = await sdRequest({ path: "x/post", params: toParams(args) });
|
|
277
|
+
return fromResponse(result, { note: `X (Twitter) post data for tweet ${args.tweetId}.` });
|
|
278
|
+
}
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// 9. Datacenter Proxies configuration helper
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
const datacenterProxySchema = {
|
|
283
|
+
target_url: z
|
|
284
|
+
.string()
|
|
285
|
+
.url()
|
|
286
|
+
.optional()
|
|
287
|
+
.describe("Optional target URL to test the proxy against. If provided, a ready-to-use curl command is returned."),
|
|
288
|
+
...apiKeyParam,
|
|
289
|
+
};
|
|
290
|
+
async function handleDatacenterProxy(args) {
|
|
291
|
+
// The datacenter proxy is not an HTTP API — it's a forward proxy you point
|
|
292
|
+
// your own HTTP client at. This tool returns the connection details and a
|
|
293
|
+
// ready-to-paste curl command so callers can use it directly.
|
|
294
|
+
const apiKey = args.api_key;
|
|
295
|
+
// We still resolve the key so callers get a clear error if none is configured,
|
|
296
|
+
// and so we can embed it in the example.
|
|
297
|
+
let resolvedKey = "";
|
|
298
|
+
try {
|
|
299
|
+
const { resolveApiKey } = await import("./scrapingdog.js");
|
|
300
|
+
resolvedKey = await resolveApiKey(apiKey);
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
return error(err instanceof Error ? err.message : String(err));
|
|
304
|
+
}
|
|
305
|
+
const host = "proxy.scrapingdog.com";
|
|
306
|
+
const port = "8081";
|
|
307
|
+
const username = "scrapingdog";
|
|
308
|
+
const password = resolvedKey;
|
|
309
|
+
const lines = [
|
|
310
|
+
"Scrapingdog Datacenter Proxy configuration:",
|
|
311
|
+
"",
|
|
312
|
+
` Host: ${host}`,
|
|
313
|
+
` Port: ${port}`,
|
|
314
|
+
` Username: ${username}`,
|
|
315
|
+
` Password: ${password} (your Scrapingdog API key)`,
|
|
316
|
+
"",
|
|
317
|
+
"Proxy URL: http://scrapingdog:" + password + "@" + host + ":" + port,
|
|
318
|
+
"",
|
|
319
|
+
"Example (curl):",
|
|
320
|
+
` curl -x "http://scrapingdog:${password}@${host}:${port}" -k "https://httpbin.org/ip"`,
|
|
321
|
+
"",
|
|
322
|
+
"Example (Python requests):",
|
|
323
|
+
' proxies = {"http": "http://scrapingdog:' + password + "@" + host + ":" + port + '",',
|
|
324
|
+
' "https": "http://scrapingdog:' + password + "@" + host + ":" + port + '"}',
|
|
325
|
+
' requests.get("https://httpbin.org/ip", proxies=proxies, verify=False)',
|
|
326
|
+
];
|
|
327
|
+
if (args.target_url) {
|
|
328
|
+
lines.push("", "Ready-to-run curl for your target URL:", ` curl -x "http://scrapingdog:${password}@${host}:${port}" -k "${args.target_url}"`);
|
|
329
|
+
}
|
|
330
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
331
|
+
}
|
|
332
|
+
const tools = [
|
|
333
|
+
{
|
|
334
|
+
name: "scrape",
|
|
335
|
+
description: "Scrape any URL via the Scrapingdog Web Scraping API (/scrape). Returns the page's HTML. " +
|
|
336
|
+
"Supports JS rendering (dynamic), premium residential proxies, geotargeting (country), " +
|
|
337
|
+
"sessions, stealth/captcha bypass, and a render wait. Costs 1–25 credits depending on options.",
|
|
338
|
+
schema: scrapeSchema,
|
|
339
|
+
handler: handleScrape,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: "screenshot",
|
|
343
|
+
description: "Capture a screenshot of a URL via the Scrapingdog Screenshot API (/screenshot). " +
|
|
344
|
+
"Supports full-page capture, viewport size, wait-until, format (png/jpg/webp), and quality. " +
|
|
345
|
+
"Costs 5 credits. Note: this tool reports image metadata; binary bytes aren't returned as text.",
|
|
346
|
+
schema: screenshotSchema,
|
|
347
|
+
handler: handleScreenshot,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
name: "google_search",
|
|
351
|
+
description: "Run a Google search via Scrapingdog (/google). Returns organic results, ads, knowledge graph, " +
|
|
352
|
+
"and other SERP features as JSON (or raw HTML with html=true). Supports localization, " +
|
|
353
|
+
"pagination, advanced filters, and mobile results. Costs 5–10 credits.",
|
|
354
|
+
schema: googleSearchSchema,
|
|
355
|
+
handler: handleGoogleSearch,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "bing_search",
|
|
359
|
+
description: "Run a Bing search via Scrapingdog (/bing/search). Returns parsed Bing results as JSON. " +
|
|
360
|
+
"Supports market/country targeting, geo coordinates, pagination, and safe-search filters.",
|
|
361
|
+
schema: bingSearchSchema,
|
|
362
|
+
handler: handleBingSearch,
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
name: "duckduckgo_search",
|
|
366
|
+
description: "Run a DuckDuckGo search via Scrapingdog (/duckduckgo/search). Returns parsed results as JSON. " +
|
|
367
|
+
"Supports region (kl), date filters (df), and pagination via next_page_token (15 per page).",
|
|
368
|
+
schema: duckduckgoSearchSchema,
|
|
369
|
+
handler: handleDuckDuckGoSearch,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "baidu_search",
|
|
373
|
+
description: "Run a Baidu search via Scrapingdog (/baidu/search). Returns parsed results as JSON. " +
|
|
374
|
+
"Supports Chinese-language restriction (ct), pagination (pn), and result count (rn).",
|
|
375
|
+
schema: baiduSearchSchema,
|
|
376
|
+
handler: handleBaiduSearch,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "x_profile",
|
|
380
|
+
description: "Scrape an X (Twitter) profile via Scrapingdog (/x/profile). Returns the profile's name, handle, " +
|
|
381
|
+
"follower counts, bio, and other public metadata as JSON. Pass a username or user ID.",
|
|
382
|
+
schema: xProfileSchema,
|
|
383
|
+
handler: handleXProfile,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: "x_post",
|
|
387
|
+
description: "Scrape an X (Twitter) post (tweet) via Scrapingdog (/x/post). Pass the numeric tweet ID from the post URL.",
|
|
388
|
+
schema: xPostSchema,
|
|
389
|
+
handler: handleXPost,
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "datacenter_proxy",
|
|
393
|
+
description: "Return Scrapingdog Datacenter Proxy connection details (host, port, username, password) and a " +
|
|
394
|
+
"ready-to-paste curl/Python example. This is a forward proxy (proxy.scrapingdog.com:8081), not an " +
|
|
395
|
+
"HTTP API — point your own HTTP client at it. Optionally pass target_url for a ready-to-run curl.",
|
|
396
|
+
schema: datacenterProxySchema,
|
|
397
|
+
handler: handleDatacenterProxy,
|
|
398
|
+
},
|
|
399
|
+
];
|
|
400
|
+
/** Register every tool on the given MCP server. */
|
|
401
|
+
export function registerTools(server) {
|
|
402
|
+
for (const tool of tools) {
|
|
403
|
+
server.tool(tool.name, tool.description, tool.schema, async (args) => {
|
|
404
|
+
try {
|
|
405
|
+
return await tool.handler(args);
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
409
|
+
return error(`Tool '${tool.name}' failed: ${message}`);
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Re-export for tests / external use.
|
|
415
|
+
export { sdRequest };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@0pen1/scrapingdog-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Scrapingdog — web scraping, screenshots, and Google/Bing/DuckDuckGo/Baidu search tools.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "0pen1",
|
|
8
|
+
"homepage": "https://github.com/0pen1/scrapingdog-mcp#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+ssh://git@github.com:0pen1/scrapingdog-mcp.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/0pen1/scrapingdog-mcp/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"mcp",
|
|
18
|
+
"mcp-server",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"scrapingdog",
|
|
21
|
+
"web-scraping",
|
|
22
|
+
"serp",
|
|
23
|
+
"claude",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"claude-desktop"
|
|
26
|
+
],
|
|
27
|
+
"main": "dist/index.js",
|
|
28
|
+
"bin": {
|
|
29
|
+
"scrapingdog-mcp": "dist/index.js"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsc",
|
|
38
|
+
"start": "node dist/index.js",
|
|
39
|
+
"dev": "tsc && node dist/index.js",
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"prepack": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.17"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
48
|
+
"zod": "^3.25.76"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.11.0",
|
|
52
|
+
"typescript": "^5.4.0"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public",
|
|
56
|
+
"registry": "https://registry.npmjs.org/"
|
|
57
|
+
}
|
|
58
|
+
}
|