@imenam/simple-scraper 1.0.13 → 1.0.15
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 +15 -1
- package/dist/browser.js +24 -0
- package/dist/gui.js +3 -1
- package/dist/index.js +59 -6
- package/dist/sessions.js +15 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,11 +37,15 @@ simple-scraper
|
|
|
37
37
|
|----------|----------|---------|-------------|
|
|
38
38
|
| `PUPPETEER_HEADLESS` | No | `true` | Run Chromium in headless mode. Set to `false` to display the browser window. |
|
|
39
39
|
| `PUPPETEER_TIMEOUT` | No | `30000` | Default timeout in milliseconds for page navigation and waits. |
|
|
40
|
+
| `PUPPETEER_VIEWPORT_WIDTH` | No | `1280` | Viewport width in CSS pixels, applied to every page. Affects responsive layouts and lazy-loading, not just screenshots. |
|
|
41
|
+
| `PUPPETEER_VIEWPORT_HEIGHT` | No | `800` | Viewport height in CSS pixels, applied to every page. |
|
|
42
|
+
| `PUPPETEER_DEVICE_SCALE_FACTOR` | No | `1` | Physical pixels per CSS pixel. Set to `2` for retina-density screenshots (same framing, twice the detail, ~4× the file size). |
|
|
40
43
|
| `COOKIES_DIR` | No | - | Absolute path to a folder containing Netscape-format `.txt` cookie files. All files are loaded and merged automatically before each request. |
|
|
41
44
|
| `MCP_LOG_DIR` | No | `.mcp-gui/logs` | Absolute path to the directory where log files are written. |
|
|
42
45
|
| `PROXY_URL` | No | - | Base URL of the [MCP HTTP Gateway](https://www.npmjs.com/package/@imenam/mcp-http-gateway). Required to enable the GUI. |
|
|
43
46
|
| `PROXY_APP_PATH` | No | `/simple-scraper-mcp` | URL path under which the GUI is registered on the proxy. |
|
|
44
47
|
| `PROXY_APP_NAME` | No | `Simple Scraper MCP` | Display name shown in the proxy's app list. |
|
|
48
|
+
| `APP_GROUP` | No | - | Groups the app under a collapsible section (folded by default) in the proxy dashboard. Apps sharing the same value appear together; if unset, the app lands in the default "ungrouped" section. |
|
|
45
49
|
| `SCRAPER_MAX_SESSIONS` | No | `5` | Maximum number of concurrent interactive sessions. |
|
|
46
50
|
| `SCRAPER_SESSION_TTL_MS` | No | `600000` | Inactivity TTL for sessions in milliseconds (default: 10 minutes). Sessions unused beyond this duration are closed automatically. |
|
|
47
51
|
|
|
@@ -53,6 +57,9 @@ Copy `.env.example` to `.env` and configure the variables:
|
|
|
53
57
|
# Puppeteer options (optional)
|
|
54
58
|
PUPPETEER_HEADLESS=true
|
|
55
59
|
PUPPETEER_TIMEOUT=30000
|
|
60
|
+
PUPPETEER_VIEWPORT_WIDTH=1280
|
|
61
|
+
PUPPETEER_VIEWPORT_HEIGHT=800
|
|
62
|
+
PUPPETEER_DEVICE_SCALE_FACTOR=1
|
|
56
63
|
|
|
57
64
|
# Optional: path to a folder containing Netscape-format cookie files (.txt)
|
|
58
65
|
# All files in this folder will be loaded automatically before each request.
|
|
@@ -62,6 +69,8 @@ PUPPETEER_TIMEOUT=30000
|
|
|
62
69
|
# PROXY_URL=http://localhost:3000
|
|
63
70
|
# PROXY_APP_PATH=/simple-scraper-mcp
|
|
64
71
|
# PROXY_APP_NAME=Simple Scraper MCP
|
|
72
|
+
# Optional: groups this app under a collapsible section of the proxy dashboard
|
|
73
|
+
# APP_GROUP=Scraping
|
|
65
74
|
```
|
|
66
75
|
|
|
67
76
|
## Usage with Claude Desktop
|
|
@@ -229,6 +238,7 @@ Open a persistent browser session. Returns a `session_id` to use with all `sessi
|
|
|
229
238
|
| `url` | string | ✅ | URL to navigate to |
|
|
230
239
|
| `wait_for` | string | | CSS selector to wait for before the session is considered ready |
|
|
231
240
|
| `timeout` | number | | Timeout in ms (default: 30000) |
|
|
241
|
+
| `viewport` | object | | Viewport override for this session, applied before navigation: `{ width, height, device_scale_factor }`. Omitted fields fall back to the server defaults. |
|
|
232
242
|
|
|
233
243
|
### `close_session`
|
|
234
244
|
|
|
@@ -308,6 +318,8 @@ Return the current full rendered HTML of the session page.
|
|
|
308
318
|
|
|
309
319
|
Capture a screenshot of a page. Use `session_id` to capture an active session in its current state, or `url` for a one-shot capture.
|
|
310
320
|
|
|
321
|
+
The capture area is resolved by precedence: `selector` → `clip` → `full_page` → the visible viewport.
|
|
322
|
+
|
|
311
323
|
| Parameter | Type | Required | Description |
|
|
312
324
|
|-----------|------|----------|-------------|
|
|
313
325
|
| `session_id` | string | | Session ID. If provided, `url` is ignored and the current page state is captured. |
|
|
@@ -315,7 +327,9 @@ Capture a screenshot of a page. Use `session_id` to capture an active session in
|
|
|
315
327
|
| `wait_for` | string | | CSS selector to wait for (one-shot mode only) |
|
|
316
328
|
| `timeout` | number | | Timeout in ms (default: 30000) |
|
|
317
329
|
| `selector` | string | | CSS selector of a specific element to capture |
|
|
318
|
-
| `
|
|
330
|
+
| `clip` | object | | Arbitrary pixel region `{ x, y, width, height }` in CSS pixels, measured from the top-left of the page. May extend below the visible viewport. Ignored when `selector` is set; cannot be combined with `full_page`. |
|
|
331
|
+
| `viewport` | object | | Viewport override for this capture: `{ width, height, device_scale_factor }`. For a session, the previous viewport is restored afterwards. |
|
|
332
|
+
| `full_page` | boolean | | Capture the full scrollable page height (default: false, ignored when `selector` or `clip` is provided) |
|
|
319
333
|
| `format` | `png` \| `jpeg` | | Image format (default: `png`) |
|
|
320
334
|
| `output` | `inline` \| `file` \| `both` | ✅ | `inline` embeds the image in the response, `file` saves to disk and returns the path, `both` does both |
|
|
321
335
|
| `path` | string | | Absolute or relative path for the saved file. Defaults to `./screenshots/screenshot-<timestamp>.<format>` |
|
package/dist/browser.js
CHANGED
|
@@ -4,6 +4,29 @@ export function getDefaultTimeout() {
|
|
|
4
4
|
const val = process.env.PUPPETEER_TIMEOUT;
|
|
5
5
|
return val ? parseInt(val, 10) : 30_000;
|
|
6
6
|
}
|
|
7
|
+
function envNumber(name, fallback) {
|
|
8
|
+
const raw = process.env[name];
|
|
9
|
+
if (!raw)
|
|
10
|
+
return fallback;
|
|
11
|
+
const parsed = Number(raw);
|
|
12
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
13
|
+
console.error(`[Browser] Ignoring invalid ${name}="${raw}", using ${fallback}`);
|
|
14
|
+
return fallback;
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Default viewport applied to every page. Puppeteer's own default is 800x600,
|
|
20
|
+
* which is narrow enough to trigger mobile/tablet layouts on many sites — that
|
|
21
|
+
* affects scraping (lazy-load, responsive hiding), not just screenshots.
|
|
22
|
+
*/
|
|
23
|
+
export function getDefaultViewport() {
|
|
24
|
+
return {
|
|
25
|
+
width: envNumber('PUPPETEER_VIEWPORT_WIDTH', 1280),
|
|
26
|
+
height: envNumber('PUPPETEER_VIEWPORT_HEIGHT', 800),
|
|
27
|
+
deviceScaleFactor: envNumber('PUPPETEER_DEVICE_SCALE_FACTOR', 1),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
7
30
|
export async function getBrowser() {
|
|
8
31
|
if (browser && browser.connected) {
|
|
9
32
|
return browser;
|
|
@@ -11,6 +34,7 @@ export async function getBrowser() {
|
|
|
11
34
|
const headless = process.env.PUPPETEER_HEADLESS !== 'false';
|
|
12
35
|
browser = await puppeteer.launch({
|
|
13
36
|
headless,
|
|
37
|
+
defaultViewport: getDefaultViewport(),
|
|
14
38
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
15
39
|
});
|
|
16
40
|
console.error('[Browser] Launched Puppeteer instance');
|
package/dist/gui.js
CHANGED
|
@@ -172,7 +172,9 @@ app.get('/api/sessions/:id/screenshot', async (req, res) => {
|
|
|
172
172
|
});
|
|
173
173
|
async function startServer() {
|
|
174
174
|
const client = new ProxyClient(process.env.PROXY_URL);
|
|
175
|
-
|
|
175
|
+
// APP_GROUP place la GUI dans une section repliable du proxy (facultatif).
|
|
176
|
+
const registerOptions = { path: APP_PATH, name: APP_NAME, group: process.env.APP_GROUP };
|
|
177
|
+
const result = await client.register(registerOptions);
|
|
176
178
|
// Une autre instance du serveur MCP sert déjà cette GUI. C'est le cas nominal
|
|
177
179
|
// quand le client redémarre le serveur : il lance le nouveau master avant de
|
|
178
180
|
// couper l'ancien, dont la GUI tient encore la route. Ce n'est pas un crash —
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { setupLogging } from './logger.js';
|
|
|
10
10
|
import { getBrowser, closeBrowser, getDefaultTimeout } from './browser.js';
|
|
11
11
|
import { loadAllCookies } from './cookies.js';
|
|
12
12
|
import { GuiLauncher, reconnectGuiToolDefinition } from '@imenam/mcp-gui-interface';
|
|
13
|
-
import { createSession, getSession, closeSession, closeAllSessions, listSessions, getSessionLogs, clearSessionLogs } from './sessions.js';
|
|
13
|
+
import { createSession, getSession, closeSession, closeAllSessions, listSessions, getSessionLogs, clearSessionLogs, resolveViewport } from './sessions.js';
|
|
14
14
|
import { registerSessionRpc } from './session-rpc.js';
|
|
15
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
16
|
const rootDir = path.resolve(__dirname, '..');
|
|
@@ -33,6 +33,24 @@ const server = new McpServer({
|
|
|
33
33
|
name: 'simple-scraper-mcp',
|
|
34
34
|
version: '1.0.0',
|
|
35
35
|
});
|
|
36
|
+
// Shared viewport override. Any omitted field falls back to the env-configured
|
|
37
|
+
// default (PUPPETEER_VIEWPORT_WIDTH / _HEIGHT / _DEVICE_SCALE_FACTOR).
|
|
38
|
+
const viewportSchema = z
|
|
39
|
+
.object({
|
|
40
|
+
width: z.number().int().positive().optional().describe('Viewport width in CSS pixels'),
|
|
41
|
+
height: z.number().int().positive().optional().describe('Viewport height in CSS pixels'),
|
|
42
|
+
device_scale_factor: z
|
|
43
|
+
.number()
|
|
44
|
+
.positive()
|
|
45
|
+
.optional()
|
|
46
|
+
.describe('Physical pixels per CSS pixel, e.g. 2 for retina-density output (default: 1)'),
|
|
47
|
+
})
|
|
48
|
+
.optional();
|
|
49
|
+
function toViewportOverride(v) {
|
|
50
|
+
if (!v)
|
|
51
|
+
return undefined;
|
|
52
|
+
return { width: v.width, height: v.height, deviceScaleFactor: v.device_scale_factor };
|
|
53
|
+
}
|
|
36
54
|
// ─── One-shot helpers ─────────────────────────────────────────────────────────
|
|
37
55
|
async function setCookies(page, cookies, pageUrl) {
|
|
38
56
|
const prepared = cookies.map(c => {
|
|
@@ -329,9 +347,14 @@ server.tool('open_session', 'Open a persistent browser session on a URL. The pag
|
|
|
329
347
|
url: z.string().url().describe('URL to navigate to when opening the session'),
|
|
330
348
|
wait_for: z.string().optional().describe('CSS selector to wait for before the session is considered ready'),
|
|
331
349
|
timeout: z.number().optional().describe('Timeout in milliseconds (default: 30000)'),
|
|
332
|
-
|
|
350
|
+
viewport: viewportSchema.describe('Viewport override for this session, applied before navigation. Omitted fields fall back to the server default.'),
|
|
351
|
+
}, async ({ url, wait_for, timeout, viewport }) => {
|
|
333
352
|
try {
|
|
334
|
-
const sessionId = await createSession(url, {
|
|
353
|
+
const sessionId = await createSession(url, {
|
|
354
|
+
waitFor: wait_for,
|
|
355
|
+
timeout,
|
|
356
|
+
viewport: toViewportOverride(viewport),
|
|
357
|
+
});
|
|
335
358
|
return { content: [{ type: 'text', text: JSON.stringify({ session_id: sessionId }) }] };
|
|
336
359
|
}
|
|
337
360
|
catch (error) {
|
|
@@ -550,25 +573,44 @@ server.tool('session_scroll', 'Scroll the active session page. Use selector to s
|
|
|
550
573
|
}
|
|
551
574
|
});
|
|
552
575
|
// ─── screenshot ───────────────────────────────────────────────────────────────
|
|
553
|
-
server.tool('screenshot', 'Take a screenshot of a page. Provide either session_id (captures the page in its current interactive state) or url (one-shot: navigates, captures, closes).
|
|
576
|
+
server.tool('screenshot', 'Take a screenshot of a page. Provide either session_id (captures the page in its current interactive state) or url (one-shot: navigates, captures, closes). Capture area, by precedence: selector (a specific element), clip (an arbitrary pixel region), full_page (the whole scrollable height), otherwise the visible viewport. output controls the return format: "inline" returns a base64 image directly in the response, "file" writes the image to disk and returns the path, "both" does both.', {
|
|
554
577
|
session_id: z.string().optional().describe('Session ID of an active browser session. If provided, url is ignored and the current page state is captured.'),
|
|
555
578
|
url: z.string().url().optional().describe('URL to navigate to for a one-shot screenshot (required if session_id is not provided)'),
|
|
556
579
|
wait_for: z.string().optional().describe('CSS selector to wait for before capturing (one-shot mode only)'),
|
|
557
580
|
timeout: z.number().optional().describe('Timeout in milliseconds (default: 30000)'),
|
|
558
581
|
selector: z.string().optional().describe('CSS selector of a specific element to capture instead of the full page'),
|
|
559
|
-
|
|
582
|
+
clip: z
|
|
583
|
+
.object({
|
|
584
|
+
x: z.number().describe('Left offset in CSS pixels, from the top-left of the page'),
|
|
585
|
+
y: z.number().describe('Top offset in CSS pixels, from the top-left of the page'),
|
|
586
|
+
width: z.number().positive().describe('Region width in CSS pixels'),
|
|
587
|
+
height: z.number().positive().describe('Region height in CSS pixels'),
|
|
588
|
+
})
|
|
589
|
+
.optional()
|
|
590
|
+
.describe('Arbitrary pixel region to capture. Ignored when selector is provided, and cannot be combined with full_page.'),
|
|
591
|
+
viewport: viewportSchema.describe('Viewport override for this capture. For a session, the previous viewport is restored afterwards.'),
|
|
592
|
+
full_page: z.boolean().optional().describe('Capture the full scrollable page height (default: false, ignored when selector or clip is provided)'),
|
|
560
593
|
format: z.enum(['png', 'jpeg']).optional().describe('Image format (default: png)'),
|
|
561
594
|
output: z.enum(['inline', 'file', 'both']).describe('Return format: "inline" embeds the image in the response, "file" saves to disk and returns the path, "both" does both'),
|
|
562
595
|
path: z.string().optional().describe('Absolute or relative file path for the saved image (used when output is "file" or "both"). Relative paths are resolved from the MCP server\'s working directory. Defaults to <server_root>/screenshots/<timestamp>.<format>'),
|
|
563
|
-
}, async ({ session_id, url, wait_for, timeout, selector, full_page, format, output, path: filePath }) => {
|
|
596
|
+
}, async ({ session_id, url, wait_for, timeout, selector, clip, viewport, full_page, format, output, path: filePath }) => {
|
|
564
597
|
const effectiveTimeout = timeout ?? getDefaultTimeout();
|
|
565
598
|
const fmt = format ?? 'png';
|
|
566
599
|
const mimeType = fmt === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
600
|
+
if (clip && full_page) {
|
|
601
|
+
return { content: [{ type: 'text', text: 'Error: clip and full_page cannot be combined — clip already defines the region to capture.' }], isError: true };
|
|
602
|
+
}
|
|
567
603
|
let page = null;
|
|
568
604
|
let ownedPage = false;
|
|
605
|
+
// Only set for session pages, whose viewport must survive this call unchanged.
|
|
606
|
+
let previousViewport;
|
|
569
607
|
try {
|
|
570
608
|
if (session_id) {
|
|
571
609
|
page = getSession(session_id);
|
|
610
|
+
if (viewport) {
|
|
611
|
+
previousViewport = page.viewport();
|
|
612
|
+
await page.setViewport(resolveViewport(toViewportOverride(viewport)));
|
|
613
|
+
}
|
|
572
614
|
}
|
|
573
615
|
else {
|
|
574
616
|
if (!url) {
|
|
@@ -577,6 +619,9 @@ server.tool('screenshot', 'Take a screenshot of a page. Provide either session_i
|
|
|
577
619
|
const browser = await getBrowser();
|
|
578
620
|
page = await browser.newPage();
|
|
579
621
|
ownedPage = true;
|
|
622
|
+
if (viewport) {
|
|
623
|
+
await page.setViewport(resolveViewport(toViewportOverride(viewport)));
|
|
624
|
+
}
|
|
580
625
|
await applyCookies(page, url);
|
|
581
626
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: effectiveTimeout });
|
|
582
627
|
if (wait_for) {
|
|
@@ -592,6 +637,11 @@ server.tool('screenshot', 'Take a screenshot of a page. Provide either session_i
|
|
|
592
637
|
}
|
|
593
638
|
imageBuffer = Buffer.from(await element.screenshot({ type: fmt }));
|
|
594
639
|
}
|
|
640
|
+
else if (clip) {
|
|
641
|
+
// captureBeyondViewport lets the region extend past the visible area
|
|
642
|
+
// without having to scroll the page first.
|
|
643
|
+
imageBuffer = Buffer.from(await page.screenshot({ type: fmt, clip, captureBeyondViewport: true }));
|
|
644
|
+
}
|
|
595
645
|
else {
|
|
596
646
|
imageBuffer = Buffer.from(await page.screenshot({ type: fmt, fullPage: full_page ?? false }));
|
|
597
647
|
}
|
|
@@ -624,6 +674,9 @@ server.tool('screenshot', 'Take a screenshot of a page. Provide either session_i
|
|
|
624
674
|
if (ownedPage && page) {
|
|
625
675
|
await page.close();
|
|
626
676
|
}
|
|
677
|
+
else if (page && previousViewport) {
|
|
678
|
+
await page.setViewport(previousViewport).catch(() => undefined);
|
|
679
|
+
}
|
|
627
680
|
}
|
|
628
681
|
});
|
|
629
682
|
function launchGUI() {
|
package/dist/sessions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getBrowser } from './browser.js';
|
|
1
|
+
import { getBrowser, getDefaultViewport } from './browser.js';
|
|
2
2
|
import { loadAllCookies } from './cookies.js';
|
|
3
3
|
const MAX_SESSIONS = parseInt(process.env.SCRAPER_MAX_SESSIONS ?? '5', 10);
|
|
4
4
|
const TTL_MS = parseInt(process.env.SCRAPER_SESSION_TTL_MS ?? String(10 * 60 * 1000), 10);
|
|
@@ -13,6 +13,17 @@ async function applyCookies(page, pageUrl) {
|
|
|
13
13
|
const prepared = cookies.map(c => (c.domain ? c : { ...c, url: pageUrl }));
|
|
14
14
|
await page.setCookie(...prepared);
|
|
15
15
|
}
|
|
16
|
+
/** Merge a partial override onto the env-configured default viewport. */
|
|
17
|
+
export function resolveViewport(override) {
|
|
18
|
+
const base = getDefaultViewport();
|
|
19
|
+
if (!override)
|
|
20
|
+
return base;
|
|
21
|
+
return {
|
|
22
|
+
width: override.width ?? base.width,
|
|
23
|
+
height: override.height ?? base.height,
|
|
24
|
+
deviceScaleFactor: override.deviceScaleFactor ?? base.deviceScaleFactor,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
16
27
|
export async function createSession(url, opts = {}) {
|
|
17
28
|
if (sessions.size >= MAX_SESSIONS) {
|
|
18
29
|
throw new Error(`Session limit reached (max ${MAX_SESSIONS}). Close an existing session before opening a new one.`);
|
|
@@ -25,6 +36,9 @@ export async function createSession(url, opts = {}) {
|
|
|
25
36
|
entry.consoleLogs.push({ type: msg.type(), text: msg.text(), timestamp: Date.now() });
|
|
26
37
|
});
|
|
27
38
|
try {
|
|
39
|
+
if (opts.viewport) {
|
|
40
|
+
await page.setViewport(resolveViewport(opts.viewport));
|
|
41
|
+
}
|
|
28
42
|
await applyCookies(page, url);
|
|
29
43
|
await page.goto(url, { waitUntil: 'networkidle2', timeout: opts.timeout ?? 30_000 });
|
|
30
44
|
if (opts.waitFor) {
|