scrapeless-mcp-server 0.5.0 → 0.6.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 +1 -0
- package/build/cf.js +27 -0
- package/build/config.js +11 -0
- package/build/context-manager.js +40 -0
- package/build/context.js +120 -0
- package/build/index.js +23 -0
- package/build/server.js +60 -0
- package/build/session-manager.js +76 -0
- package/build/tools/browser/browser.js +488 -0
- package/build/tools/crawl/api.js +26 -0
- package/build/tools/crawl/crawlCancel.js +19 -0
- package/build/tools/crawl/crawlResult.js +72 -0
- package/build/tools/crawl/crawlStart.js +138 -0
- package/build/tools/deepserp/googleSearch.js +37 -0
- package/build/tools/deepserp/googleTrends.js +208 -0
- package/build/tools/index.js +9 -0
- package/build/tools/llm_chat_scraper/api.js +27 -0
- package/build/tools/llm_chat_scraper/llmChatScraper.js +634 -0
- package/build/tools/universal/scrapeHtml.js +22 -0
- package/build/tools/universal/scrapeMarkdown.js +29 -0
- package/build/tools/universal/scrapeScreenshot.js +48 -0
- package/build/tools/utils.js +86 -0
- package/build/transport.js +120 -0
- package/package.json +3 -2
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool, wrapMcpBrowserResponse, snapshot, } from "../utils.js";
|
|
3
|
+
export const browserCreate = defineTool({
|
|
4
|
+
name: "browser_create",
|
|
5
|
+
description: "Create or reuse a cloud browser session using Scrapeless. Updates the active session.",
|
|
6
|
+
inputSchema: {
|
|
7
|
+
sessionId: z
|
|
8
|
+
.string()
|
|
9
|
+
.optional()
|
|
10
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, a new session is created."),
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
export const browserClose = defineTool({
|
|
14
|
+
name: "browser_close",
|
|
15
|
+
description: "Closes the current session by disconnecting the cloud browser. This will terminate the recording for the session.",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
sessionId: z
|
|
18
|
+
.string()
|
|
19
|
+
.describe("The session ID to identify target browser. If not provided or invalid, you cannot close the session."),
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
export const browserGoto = defineTool({
|
|
23
|
+
name: "browser_goto",
|
|
24
|
+
description: `Navigate browser to a specified URL.
|
|
25
|
+
Restrictions: Only for direct URL navigation, not for searches.
|
|
26
|
+
Valid: Go to https://google.com.
|
|
27
|
+
Invalid: Search for "cats" on Google (use google_search).`,
|
|
28
|
+
inputSchema: {
|
|
29
|
+
url: z
|
|
30
|
+
.string()
|
|
31
|
+
.url()
|
|
32
|
+
.describe("The URL to navigate to in the browser session."),
|
|
33
|
+
sessionId: z
|
|
34
|
+
.string()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
37
|
+
},
|
|
38
|
+
handle: async (context, params) => {
|
|
39
|
+
const session = context.getSession(params.sessionId);
|
|
40
|
+
if (!session?.page) {
|
|
41
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
42
|
+
}
|
|
43
|
+
await session.page.goto(params.url);
|
|
44
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: accessibilitySnapshot,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
export const browserGoBack = defineTool({
|
|
56
|
+
name: "browser_go_back",
|
|
57
|
+
description: `Go back one step in browser history.
|
|
58
|
+
Restrictions: Only works if a previous page exists in the session history.
|
|
59
|
+
Valid: After navigating from page A to B, go back to A.
|
|
60
|
+
Invalid: Attempting to go back on the first page of a session.`,
|
|
61
|
+
inputSchema: {
|
|
62
|
+
sessionId: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
66
|
+
},
|
|
67
|
+
handle: async (context, params) => {
|
|
68
|
+
const session = context.getSession(params.sessionId);
|
|
69
|
+
if (!session?.page) {
|
|
70
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
71
|
+
}
|
|
72
|
+
await session.page.goBack();
|
|
73
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
74
|
+
return {
|
|
75
|
+
content: [
|
|
76
|
+
{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: accessibilitySnapshot,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
export const browserGoForward = defineTool({
|
|
85
|
+
name: "browser_go_forward",
|
|
86
|
+
description: `Go forward one step in browser history.
|
|
87
|
+
Restrictions: Only works after a "go_back" action has been performed.
|
|
88
|
+
Valid: After going back from page B to A, go forward to B.
|
|
89
|
+
Invalid: Attempting to go forward without a preceding "go_back" action.`,
|
|
90
|
+
inputSchema: {
|
|
91
|
+
sessionId: z
|
|
92
|
+
.string()
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
95
|
+
},
|
|
96
|
+
handle: async (context, params) => {
|
|
97
|
+
const session = context.getSession(params.sessionId);
|
|
98
|
+
if (!session?.page) {
|
|
99
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
100
|
+
}
|
|
101
|
+
await session.page.goForward();
|
|
102
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: accessibilitySnapshot,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
export const browserClick = defineTool({
|
|
114
|
+
name: "browser_click",
|
|
115
|
+
description: `Click a specific element on the page.
|
|
116
|
+
Restrictions: Requires a valid CSS selector for the target element.
|
|
117
|
+
Valid: Click the button with selector "#submit-button".
|
|
118
|
+
Invalid: Click "the login button" without providing a selector.`,
|
|
119
|
+
inputSchema: {
|
|
120
|
+
selector: z.string().describe("The CSS selector of the element to click."),
|
|
121
|
+
sessionId: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
125
|
+
},
|
|
126
|
+
handle: async (context, params) => {
|
|
127
|
+
const session = context.getSession(params.sessionId);
|
|
128
|
+
if (!session?.page) {
|
|
129
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
await session.page.click(params.selector);
|
|
133
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: accessibilitySnapshot,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return wrapMcpBrowserResponse(`Failed to click on element: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
export const browserType = defineTool({
|
|
149
|
+
name: "browser_type",
|
|
150
|
+
description: `Type text into a specified input field.
|
|
151
|
+
Restrictions: Requires a CSS selector for an input/textarea and the text to type.
|
|
152
|
+
Valid: Type "hello world" into input "[name='q']".
|
|
153
|
+
Invalid: Type "hello world" without specifying a target field.`,
|
|
154
|
+
inputSchema: {
|
|
155
|
+
selector: z.string().describe("CSS selector for the element to type into."),
|
|
156
|
+
text: z.string().describe("Text to type."),
|
|
157
|
+
sessionId: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
161
|
+
},
|
|
162
|
+
handle: async (context, params) => {
|
|
163
|
+
const session = context.getSession(params.sessionId);
|
|
164
|
+
if (!session?.page) {
|
|
165
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
await session.page.type(params.selector, params.text);
|
|
169
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
170
|
+
return {
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: accessibilitySnapshot,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return wrapMcpBrowserResponse(`Failed to type into element: ${error.message}`);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
export const browserWaitFor = defineTool({
|
|
185
|
+
name: "browser_wait_for",
|
|
186
|
+
description: `Wait for a specific page element to appear.
|
|
187
|
+
Restrictions: Requires a valid CSS selector for the element to wait for.
|
|
188
|
+
Valid: Wait for the element "#results" to become visible.
|
|
189
|
+
Invalid: Wait for "the results to load" without a selector.`,
|
|
190
|
+
inputSchema: {
|
|
191
|
+
selector: z.string().describe("CSS selector of the element to wait for."),
|
|
192
|
+
timeout: z
|
|
193
|
+
.number()
|
|
194
|
+
.optional()
|
|
195
|
+
.describe("Maximum time to wait in milliseconds (default: 30000)."),
|
|
196
|
+
sessionId: z
|
|
197
|
+
.string()
|
|
198
|
+
.optional()
|
|
199
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
200
|
+
},
|
|
201
|
+
handle: async (context, params) => {
|
|
202
|
+
const session = context.getSession(params.sessionId);
|
|
203
|
+
if (!session?.page) {
|
|
204
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
await session.page.waitForSelector(params.selector, {
|
|
208
|
+
timeout: params.timeout ?? 30000,
|
|
209
|
+
});
|
|
210
|
+
return wrapMcpBrowserResponse(`Successfully waited for element ${params.selector}`);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
return wrapMcpBrowserResponse(`Failed to wait for condition: ${error.message}`);
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
export const browserWait = defineTool({
|
|
218
|
+
name: "browser_wait",
|
|
219
|
+
description: `Pause execution for a fixed duration.
|
|
220
|
+
Restrictions: Requires a duration in milliseconds. Should be used sparingly.
|
|
221
|
+
Valid: Wait for 2000 milliseconds.
|
|
222
|
+
Invalid: Wait for a page to finish loading (use "browser_wait_for")`,
|
|
223
|
+
inputSchema: {
|
|
224
|
+
time: z
|
|
225
|
+
.number()
|
|
226
|
+
.int()
|
|
227
|
+
.positive()
|
|
228
|
+
.describe("Time to wait in milliseconds (default: 30000)."),
|
|
229
|
+
sessionId: z
|
|
230
|
+
.string()
|
|
231
|
+
.optional()
|
|
232
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
233
|
+
},
|
|
234
|
+
handle: async (context, params) => {
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, params.time ?? 30000));
|
|
236
|
+
return wrapMcpBrowserResponse(`Waited for ${params.time ?? 30000} milliseconds`);
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
export const browserSnapshot = defineTool({
|
|
240
|
+
name: "browser_snapshot",
|
|
241
|
+
description: `Capture the complete structure of a webpage, including DOM and resources, for inspection and analysis.`,
|
|
242
|
+
inputSchema: {
|
|
243
|
+
sessionId: z
|
|
244
|
+
.string()
|
|
245
|
+
.optional()
|
|
246
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
247
|
+
},
|
|
248
|
+
handle: async (context, params) => {
|
|
249
|
+
const session = context.getSession(params.sessionId);
|
|
250
|
+
if (!session?.page) {
|
|
251
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{
|
|
258
|
+
type: "text",
|
|
259
|
+
text: accessibilitySnapshot,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
return {
|
|
266
|
+
content: [],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
export const browserScreenshot = defineTool({
|
|
272
|
+
name: "browser_screenshot",
|
|
273
|
+
description: `Capture a screenshot of the current page.
|
|
274
|
+
Restrictions: Can capture either the full page or the visible viewport.
|
|
275
|
+
Valid: Take a screenshot of the current browser view.
|
|
276
|
+
Invalid: Capture a screenshot of a specific element (not supported).`,
|
|
277
|
+
inputSchema: {
|
|
278
|
+
fullPage: z
|
|
279
|
+
.boolean()
|
|
280
|
+
.optional()
|
|
281
|
+
.describe([
|
|
282
|
+
"Whether to screenshot the full page (default: false)",
|
|
283
|
+
"You should avoid fullscreen if it's not important, since the " +
|
|
284
|
+
"images can be quite large",
|
|
285
|
+
].join("\n")),
|
|
286
|
+
sessionId: z
|
|
287
|
+
.string()
|
|
288
|
+
.optional()
|
|
289
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
290
|
+
},
|
|
291
|
+
handle: async (context, params) => {
|
|
292
|
+
const session = context.getSession(params.sessionId);
|
|
293
|
+
if (!session?.page) {
|
|
294
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const screenshot = await session.page.screenshot({
|
|
298
|
+
fullPage: params.fullPage ?? false,
|
|
299
|
+
});
|
|
300
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "image",
|
|
305
|
+
// @ts-ignore
|
|
306
|
+
data: screenshot.toString("base64"),
|
|
307
|
+
mimeType: "image/png",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: accessibilitySnapshot,
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return {
|
|
318
|
+
content: [],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
export const browserGetHtml = defineTool({
|
|
324
|
+
name: "browser_get_html",
|
|
325
|
+
description: `Get the full HTML of the current page.
|
|
326
|
+
Restrictions: Returns the entire raw HTML source code.
|
|
327
|
+
Valid: Get the HTML of the current page to parse its structure.
|
|
328
|
+
Invalid: Get only the visible text (use "browser_get_text")`,
|
|
329
|
+
inputSchema: {
|
|
330
|
+
sessionId: z
|
|
331
|
+
.string()
|
|
332
|
+
.optional()
|
|
333
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
334
|
+
},
|
|
335
|
+
handle: async (context, params) => {
|
|
336
|
+
const session = context.getSession(params.sessionId);
|
|
337
|
+
if (!session?.page) {
|
|
338
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
339
|
+
}
|
|
340
|
+
try {
|
|
341
|
+
const html = await session.page.content();
|
|
342
|
+
return wrapMcpBrowserResponse(html);
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
return wrapMcpBrowserResponse(`Failed to retrieve HTML content: ${error.message}`);
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
export const browserGetText = defineTool({
|
|
350
|
+
name: "browser_get_text",
|
|
351
|
+
description: `Get all visible text from the current page.
|
|
352
|
+
Restrictions: Extracts only text content, ignoring HTML tags.
|
|
353
|
+
Valid: Get the text content of the current page for summarization.
|
|
354
|
+
Invalid: Get the page's HTML structure (use "browser_get_html")`,
|
|
355
|
+
inputSchema: {
|
|
356
|
+
sessionId: z
|
|
357
|
+
.string()
|
|
358
|
+
.optional()
|
|
359
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
360
|
+
},
|
|
361
|
+
handle: async (context, params) => {
|
|
362
|
+
const session = context.getSession(params.sessionId);
|
|
363
|
+
if (!session?.page) {
|
|
364
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
365
|
+
}
|
|
366
|
+
try {
|
|
367
|
+
const text = await session.page.evaluate(() => document.body.innerText);
|
|
368
|
+
return wrapMcpBrowserResponse(text);
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
return wrapMcpBrowserResponse(`Failed to retrieve text content: ${error.message}`);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
export const browserScroll = defineTool({
|
|
376
|
+
name: "browser_scroll",
|
|
377
|
+
description: `Scroll the current page to a specific position.
|
|
378
|
+
Restrictions: Requires pixel coordinates for scrolling.
|
|
379
|
+
Valid: Scroll to the bottom of the page (e.g., { x: 0, y: 10000 }).
|
|
380
|
+
Invalid: Scroll to "the top" without specifying coordinates.`,
|
|
381
|
+
inputSchema: {
|
|
382
|
+
sessionId: z
|
|
383
|
+
.string()
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
386
|
+
},
|
|
387
|
+
handle: async (context, params) => {
|
|
388
|
+
const session = context.getSession(params.sessionId);
|
|
389
|
+
if (!session?.page) {
|
|
390
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
await session.page.evaluate(() => {
|
|
394
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
395
|
+
});
|
|
396
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
397
|
+
return {
|
|
398
|
+
content: [
|
|
399
|
+
{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: accessibilitySnapshot,
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
return wrapMcpBrowserResponse(`Failed to scroll page: ${error.message}`);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
export const browserScrollTo = defineTool({
|
|
412
|
+
name: "browser_scroll_to",
|
|
413
|
+
description: `Scroll a specific element into view.
|
|
414
|
+
Restrictions: Requires a valid CSS selector for the target element.
|
|
415
|
+
Valid: Scroll to the element "#footer".
|
|
416
|
+
Invalid: Scroll to the bottom of the page (use "browser_scroll").`,
|
|
417
|
+
inputSchema: {
|
|
418
|
+
x: z.number().describe("X coordinate to scroll to."),
|
|
419
|
+
y: z.number().describe("Y coordinate to scroll to."),
|
|
420
|
+
sessionId: z
|
|
421
|
+
.string()
|
|
422
|
+
.optional()
|
|
423
|
+
.describe("Optional session ID to use/reuse. If not provided or invalid, the active session is used."),
|
|
424
|
+
},
|
|
425
|
+
handle: async (context, params) => {
|
|
426
|
+
const session = context.getSession(params.sessionId);
|
|
427
|
+
if (!session?.page) {
|
|
428
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
await session.page.evaluate((x, y) => {
|
|
432
|
+
window.scrollTo(x, y);
|
|
433
|
+
}, params.x, params.y);
|
|
434
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
435
|
+
return {
|
|
436
|
+
content: [
|
|
437
|
+
{
|
|
438
|
+
type: "text",
|
|
439
|
+
text: accessibilitySnapshot,
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
return wrapMcpBrowserResponse(`Failed to scroll page: ${error.message}`);
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
export const browserPressKey = defineTool({
|
|
450
|
+
name: "browser_press_key",
|
|
451
|
+
description: `Simulate a key press.
|
|
452
|
+
Restrictions: Must specify a valid key name; optional target selector.
|
|
453
|
+
Valid: Press Enter in #search.
|
|
454
|
+
Invalid: Press a key without specifying the key name.`,
|
|
455
|
+
inputSchema: {
|
|
456
|
+
selector: z
|
|
457
|
+
.string()
|
|
458
|
+
.describe("The CSS selector of the element to focus.")
|
|
459
|
+
.optional(),
|
|
460
|
+
key: z
|
|
461
|
+
.string()
|
|
462
|
+
.describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`"),
|
|
463
|
+
},
|
|
464
|
+
handle: async (context, params) => {
|
|
465
|
+
const session = context.getSession(params.sessionId);
|
|
466
|
+
if (!session?.page) {
|
|
467
|
+
return wrapMcpBrowserResponse("No active browser session found. Please create a browser session first.");
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
if (params.selector) {
|
|
471
|
+
await session.page.focus(params.selector);
|
|
472
|
+
}
|
|
473
|
+
await session.page.keyboard.press(params.key);
|
|
474
|
+
const accessibilitySnapshot = await snapshot(session.page);
|
|
475
|
+
return {
|
|
476
|
+
content: [
|
|
477
|
+
{
|
|
478
|
+
type: "text",
|
|
479
|
+
text: accessibilitySnapshot,
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
return wrapMcpBrowserResponse(`Failed to press key: ${error.message}`);
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { API_KEY, BASE_URL, API_KEY_NAME } from "../../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build an axios instance targeting the Scrapeless v2 crawler API.
|
|
5
|
+
*
|
|
6
|
+
* The `x-api-token` is resolved from the per-request client when available
|
|
7
|
+
* (so multi-tenant HTTP mode keeps using the caller's key) and falls back to
|
|
8
|
+
* the `SCRAPELESS_KEY` environment variable.
|
|
9
|
+
*/
|
|
10
|
+
export function getCrawlApi(client, headers) {
|
|
11
|
+
// Priority: explicit request header (HTTP multi-tenant) -> key carried by the
|
|
12
|
+
// per-request client -> SCRAPELESS_KEY env (stdio / fallback).
|
|
13
|
+
const apiKey = headers?.[API_KEY_NAME] ||
|
|
14
|
+
client?.scrapingCrawl?.crawl?.apiKey ||
|
|
15
|
+
API_KEY ||
|
|
16
|
+
"";
|
|
17
|
+
return axios.create({
|
|
18
|
+
baseURL: BASE_URL,
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
[API_KEY_NAME]: apiKey,
|
|
22
|
+
},
|
|
23
|
+
timeout: 30000,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export const CRAWL_ENDPOINT = "/api/v2/crawler/crawl";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineTool, wrapMcpResponse } from "../utils.js";
|
|
2
|
+
import { getCrawlApi, CRAWL_ENDPOINT } from "./api.js";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
export const crawlCancel = defineTool({
|
|
5
|
+
name: "crawl_cancel",
|
|
6
|
+
description: `Cancel an in-progress crawl job by its id (the id returned by crawl_start). Returns the cancelled status.`,
|
|
7
|
+
inputSchema: {
|
|
8
|
+
id: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("The crawl job id returned when the job was created."),
|
|
11
|
+
},
|
|
12
|
+
handle: async (params, client, headers) => {
|
|
13
|
+
return wrapMcpResponse(async () => {
|
|
14
|
+
const api = getCrawlApi(client, headers);
|
|
15
|
+
const { data } = await api.delete(`${CRAWL_ENDPOINT}/${params.id}`);
|
|
16
|
+
return data;
|
|
17
|
+
});
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { defineTool } from "../utils.js";
|
|
2
|
+
import { getCrawlApi, CRAWL_ENDPOINT } from "./api.js";
|
|
3
|
+
import z from "zod";
|
|
4
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
const TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
|
|
6
|
+
export const crawlResult = defineTool({
|
|
7
|
+
name: "crawl_result",
|
|
8
|
+
description: `Fetch the result of a crawl job by its id (the id returned by crawl_start).
|
|
9
|
+
Polls the job status until it reaches a terminal state (completed / failed / cancelled) or the timeout is reached.
|
|
10
|
+
If the timeout is reached before the job finishes, the latest status is returned along with the job id so you can retry later.`,
|
|
11
|
+
inputSchema: {
|
|
12
|
+
id: z
|
|
13
|
+
.string()
|
|
14
|
+
.describe("The crawl job id returned when the job was created."),
|
|
15
|
+
timeout: z
|
|
16
|
+
.number()
|
|
17
|
+
.optional()
|
|
18
|
+
.describe("Maximum time to wait in seconds before returning the latest result. Defaults to 300 (5 minutes)."),
|
|
19
|
+
pollInterval: z
|
|
20
|
+
.number()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Interval between status checks in seconds. Defaults to 5 seconds."),
|
|
23
|
+
},
|
|
24
|
+
handle: async (params, client, headers) => {
|
|
25
|
+
const { id } = params;
|
|
26
|
+
const timeoutMs = (params.timeout ?? 300) * 1000;
|
|
27
|
+
const pollIntervalMs = (params.pollInterval ?? 5) * 1000;
|
|
28
|
+
const api = getCrawlApi(client, headers);
|
|
29
|
+
const deadline = Date.now() + timeoutMs;
|
|
30
|
+
try {
|
|
31
|
+
let data;
|
|
32
|
+
while (true) {
|
|
33
|
+
const response = await api.get(`${CRAWL_ENDPOINT}/${id}`);
|
|
34
|
+
data = response.data;
|
|
35
|
+
if (data && TERMINAL_STATUSES.includes(data.status)) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `Response:\n\n${JSON.stringify(data)}`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (Date.now() + pollIntervalMs >= deadline) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: `The crawl job did not finish within the timeout. ` +
|
|
51
|
+
`Latest status is "${data?.status}". You can call crawl_result again ` +
|
|
52
|
+
`with this job id to keep waiting, or crawl_cancel to cancel it.\n\n` +
|
|
53
|
+
`Job ID: ${id}\n\nLatest response:\n\n${JSON.stringify(data)}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
await sleep(pollIntervalMs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `Failed to fetch crawl result for job ${id}. Error: ${error.message}`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|