shadey-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/README.md +56 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +308 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Shadey MCP Server
|
|
2
|
+
|
|
3
|
+
Stealth browser for AI agents. Bypasses Cloudflare, DataDome, and every anti-bot system.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @anthropic-ai/shadey-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Set your API key:
|
|
12
|
+
```bash
|
|
13
|
+
export SHADEY_API_KEY=sk_your_key
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Claude Desktop
|
|
17
|
+
|
|
18
|
+
Add to `claude_desktop_config.json`:
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"shadey": {
|
|
24
|
+
"command": "npx",
|
|
25
|
+
"args": ["@anthropic-ai/shadey-mcp"],
|
|
26
|
+
"env": {
|
|
27
|
+
"SHADEY_API_KEY": "sk_your_key"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Tools
|
|
35
|
+
|
|
36
|
+
| Tool | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `browse` | Navigate to any URL through a stealth browser |
|
|
39
|
+
| `extract` | Extract data using CSS selectors |
|
|
40
|
+
| `screenshot` | Take a screenshot of the current page |
|
|
41
|
+
| `evaluate` | Run JavaScript in the page |
|
|
42
|
+
| `content` | Get full page HTML |
|
|
43
|
+
| `click` | Click with human-like mouse movement |
|
|
44
|
+
| `type_text` | Type with realistic keystroke timing |
|
|
45
|
+
| `scroll` | Scroll with natural momentum |
|
|
46
|
+
| `close_session` | Close browser and destroy all data |
|
|
47
|
+
|
|
48
|
+
## Example
|
|
49
|
+
|
|
50
|
+
Ask Claude: "Go to github.com/torvalds and tell me about his pinned repositories"
|
|
51
|
+
|
|
52
|
+
Claude will use the `browse` tool to navigate, then `extract` to pull data — all through a stealth browser that passes anti-bot detection.
|
|
53
|
+
|
|
54
|
+
## Get an API key
|
|
55
|
+
|
|
56
|
+
Sign up at [shadey.dev](https://shadey.dev)
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
|
+
const API_URL = process.env.SHADEY_API_URL || "https://api.shadey.dev";
|
|
8
|
+
const API_KEY = process.env.SHADEY_API_KEY || "";
|
|
9
|
+
if (!API_KEY) {
|
|
10
|
+
console.error("SHADEY_API_KEY environment variable required");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
async function shadeyFetch(path, opts = {}) {
|
|
14
|
+
const res = await fetch(`${API_URL}${path}`, {
|
|
15
|
+
...opts,
|
|
16
|
+
headers: {
|
|
17
|
+
"Content-Type": "application/json",
|
|
18
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
19
|
+
...opts.headers,
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
if (res.status === 204)
|
|
23
|
+
return {};
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
// Active session for multi-step browsing
|
|
27
|
+
let activeSession = null;
|
|
28
|
+
async function ensureSession() {
|
|
29
|
+
if (activeSession)
|
|
30
|
+
return activeSession;
|
|
31
|
+
const data = await shadeyFetch("/v1/sessions", {
|
|
32
|
+
method: "POST",
|
|
33
|
+
body: JSON.stringify({}),
|
|
34
|
+
});
|
|
35
|
+
activeSession = data.id;
|
|
36
|
+
return data.id;
|
|
37
|
+
}
|
|
38
|
+
async function destroySession() {
|
|
39
|
+
if (activeSession) {
|
|
40
|
+
await shadeyFetch(`/v1/sessions/${activeSession}`, { method: "DELETE" }).catch(() => { });
|
|
41
|
+
activeSession = null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const server = new index_js_1.Server({ name: "shadey-browser", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
45
|
+
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
46
|
+
tools: [
|
|
47
|
+
{
|
|
48
|
+
name: "browse",
|
|
49
|
+
description: "Navigate to a URL using a stealth browser that bypasses Cloudflare, DataDome, and other anti-bot systems. Returns the page title, URL, and content. The browser has a unique fingerprint and human-like behavioral patterns per session.",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: {
|
|
53
|
+
url: { type: "string", description: "URL to navigate to" },
|
|
54
|
+
wait_ms: { type: "number", description: "Milliseconds to wait after navigation (default 3000)", default: 3000 },
|
|
55
|
+
},
|
|
56
|
+
required: ["url"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "extract",
|
|
61
|
+
description: "Extract data from the current page using CSS selectors. Must call 'browse' first to navigate to a page.",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
selector: { type: "string", description: "CSS selector to extract" },
|
|
66
|
+
attribute: { type: "string", description: "HTML attribute to extract (default: textContent)", default: "textContent" },
|
|
67
|
+
all: { type: "boolean", description: "Extract all matching elements (default: false)", default: false },
|
|
68
|
+
},
|
|
69
|
+
required: ["selector"],
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "screenshot",
|
|
74
|
+
description: "Take a screenshot of the current page. Must call 'browse' first. Returns a JPEG image.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "evaluate",
|
|
82
|
+
description: "Run JavaScript in the current page context. Must call 'browse' first.",
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
expression: { type: "string", description: "JavaScript to evaluate" },
|
|
87
|
+
},
|
|
88
|
+
required: ["expression"],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "content",
|
|
93
|
+
description: "Get the full HTML of the current page. Must call 'browse' first.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "click",
|
|
101
|
+
description: "Click at coordinates on the current page with human-like mouse movement (Fitts' law trajectory, corrective submovements). Must call 'browse' first.",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: "object",
|
|
104
|
+
properties: {
|
|
105
|
+
x: { type: "number", description: "X coordinate" },
|
|
106
|
+
y: { type: "number", description: "Y coordinate" },
|
|
107
|
+
},
|
|
108
|
+
required: ["x", "y"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "type_text",
|
|
113
|
+
description: "Type text with human-like keystroke timing (QWERTY bigram model). Must call 'browse' first.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
text: { type: "string", description: "Text to type" },
|
|
118
|
+
},
|
|
119
|
+
required: ["text"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "scroll",
|
|
124
|
+
description: "Scroll the page with human-like momentum. Positive = down, negative = up. Must call 'browse' first.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
distance: { type: "number", description: "Pixels to scroll (positive = down)" },
|
|
129
|
+
},
|
|
130
|
+
required: ["distance"],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "close_session",
|
|
135
|
+
description: "Close the current browser session and destroy all data.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: {},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
}));
|
|
143
|
+
server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
|
|
144
|
+
const { name, arguments: args } = request.params;
|
|
145
|
+
try {
|
|
146
|
+
switch (name) {
|
|
147
|
+
case "browse": {
|
|
148
|
+
const sid = await ensureSession();
|
|
149
|
+
const url = args.url;
|
|
150
|
+
const wait_ms = args.wait_ms || 3000;
|
|
151
|
+
const nav = await shadeyFetch(`/v1/sessions/${sid}/navigate`, {
|
|
152
|
+
method: "POST",
|
|
153
|
+
body: JSON.stringify({ url, wait_ms }),
|
|
154
|
+
});
|
|
155
|
+
// Also get page content for the LLM
|
|
156
|
+
const content = await shadeyFetch(`/v1/sessions/${sid}/content`, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
});
|
|
159
|
+
// Truncate HTML to avoid token overflow
|
|
160
|
+
const html = (content.html || "").slice(0, 50000);
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: `Navigated to: ${nav.url}\nTitle: ${nav.title}\n\nPage HTML (truncated to 50KB):\n${html}`,
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
case "extract": {
|
|
171
|
+
const sid = await ensureSession();
|
|
172
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/extract`, {
|
|
173
|
+
method: "POST",
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
selector: args.selector,
|
|
176
|
+
attribute: args.attribute || "textContent",
|
|
177
|
+
all: args.all || false,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{
|
|
183
|
+
type: "text",
|
|
184
|
+
text: JSON.stringify(data, null, 2),
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
case "screenshot": {
|
|
190
|
+
const sid = await ensureSession();
|
|
191
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/screenshot`, {
|
|
192
|
+
method: "POST",
|
|
193
|
+
});
|
|
194
|
+
// Return as base64 image
|
|
195
|
+
const base64 = (data.data || "").replace("data:image/jpeg;base64,", "");
|
|
196
|
+
return {
|
|
197
|
+
content: [
|
|
198
|
+
{
|
|
199
|
+
type: "image",
|
|
200
|
+
data: base64,
|
|
201
|
+
mimeType: "image/jpeg",
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
case "evaluate": {
|
|
207
|
+
const sid = await ensureSession();
|
|
208
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/evaluate`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
body: JSON.stringify({ expression: args.expression }),
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
content: [
|
|
214
|
+
{
|
|
215
|
+
type: "text",
|
|
216
|
+
text: JSON.stringify(data, null, 2),
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "content": {
|
|
222
|
+
const sid = await ensureSession();
|
|
223
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/content`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
});
|
|
226
|
+
const html = (data.html || "").slice(0, 100000);
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{
|
|
230
|
+
type: "text",
|
|
231
|
+
text: `URL: ${data.url}\nLength: ${data.length} bytes\n\n${html}`,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
case "click": {
|
|
237
|
+
const sid = await ensureSession();
|
|
238
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/behave`, {
|
|
239
|
+
method: "POST",
|
|
240
|
+
body: JSON.stringify({
|
|
241
|
+
action: "move_and_click",
|
|
242
|
+
target_x: args.x,
|
|
243
|
+
target_y: args.y,
|
|
244
|
+
target_width: 30,
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
content: [{ type: "text", text: `Clicked at (${args.x}, ${args.y}) — ${data.commands_sent} CDP commands` }],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case "type_text": {
|
|
252
|
+
const sid = await ensureSession();
|
|
253
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/behave`, {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
action: "type",
|
|
257
|
+
text: args.text,
|
|
258
|
+
typing_context: "form",
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: "text", text: `Typed "${args.text}" — ${data.commands_sent} keystrokes` }],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case "scroll": {
|
|
266
|
+
const sid = await ensureSession();
|
|
267
|
+
const data = await shadeyFetch(`/v1/sessions/${sid}/behave`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
action: "scroll",
|
|
271
|
+
distance: args.distance,
|
|
272
|
+
}),
|
|
273
|
+
});
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text", text: `Scrolled ${args.distance}px — ${data.commands_sent} events` }],
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
case "close_session": {
|
|
279
|
+
await destroySession();
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: "text", text: "Session closed. Browser destroyed, fingerprint erased." }],
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
default:
|
|
285
|
+
return { content: [{ type: "text", text: `Unknown tool: ${name}` }] };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text", text: `Error: ${err.message}` }],
|
|
291
|
+
isError: true,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
// Cleanup on exit
|
|
296
|
+
process.on("SIGINT", async () => {
|
|
297
|
+
await destroySession();
|
|
298
|
+
process.exit(0);
|
|
299
|
+
});
|
|
300
|
+
process.on("SIGTERM", async () => {
|
|
301
|
+
await destroySession();
|
|
302
|
+
process.exit(0);
|
|
303
|
+
});
|
|
304
|
+
async function main() {
|
|
305
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
306
|
+
await server.connect(transport);
|
|
307
|
+
}
|
|
308
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shadey-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Shadey — stealth browser sessions for AI agents",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shadey-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp",
|
|
19
|
+
"browser",
|
|
20
|
+
"stealth",
|
|
21
|
+
"ai",
|
|
22
|
+
"agents",
|
|
23
|
+
"shadey",
|
|
24
|
+
"cloudflare",
|
|
25
|
+
"scraping"
|
|
26
|
+
],
|
|
27
|
+
"author": "Shadey",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"homepage": "https://shadey.dev",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^25.5.0",
|
|
35
|
+
"tsx": "^4.0.0",
|
|
36
|
+
"typescript": "^5.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|