@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 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
- | `full_page` | boolean | | Capture the full scrollable page height (default: false, ignored when `selector` is provided) |
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
- const result = await client.register({ path: APP_PATH, name: APP_NAME });
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
- }, async ({ url, wait_for, timeout }) => {
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, { waitFor: wait_for, timeout });
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). The selector parameter restricts the capture to a specific element. 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.', {
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
- full_page: z.boolean().optional().describe('Capture the full scrollable page height (default: false, ignored when selector is provided)'),
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/simple-scraper",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "MCP server for web scraping and JavaScript execution using Puppeteer",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",