leapfrog-mcp 0.6.7 → 0.6.8

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.
Files changed (2) hide show
  1. package/dist/index.js +99 -0
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -347,6 +347,105 @@ server.registerTool("session_create", {
347
347
  return err(e.message);
348
348
  }
349
349
  });
350
+ // ─── session_create_batch ────────────────────────────────────────────────────
351
+ server.registerTool("session_create_batch", {
352
+ title: "Create Multiple Browser Sessions",
353
+ description: "Create multiple isolated browser sessions concurrently — 5-10x faster than sequential session_create calls. " +
354
+ "Optionally navigate each to a URL. Returns all session IDs. " +
355
+ "A single reflow positions all windows into a unified grid after all sessions are created.",
356
+ inputSchema: z.object({
357
+ sessions: z.array(z.object({
358
+ url: z.string().optional().describe("URL to navigate after creation."),
359
+ headed: z.boolean().optional().describe("Run with visible UI."),
360
+ viewport: z.object({ width: z.number(), height: z.number() }).optional().describe("Custom viewport."),
361
+ profile: z.string().optional().describe("Profile shorthand name for persistent Chrome profile."),
362
+ pinned: z.boolean().optional().describe("Pin to prevent idle timeout."),
363
+ waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).optional().describe("Wait strategy for navigation."),
364
+ })).min(1).max(15).describe("Array of sessions to create."),
365
+ }).strict(),
366
+ }, async ({ sessions: sessionSpecs }) => {
367
+ try {
368
+ const results = [];
369
+ // Phase 1: Create all sessions concurrently
370
+ const createPromises = sessionSpecs.map(async (spec) => {
371
+ try {
372
+ const session = await sessions.createSession({
373
+ headed: spec.headed,
374
+ viewport: spec.viewport,
375
+ profile: spec.profile,
376
+ });
377
+ if (spec.pinned)
378
+ session.pinned = true;
379
+ // Inject init scripts
380
+ if (LEAP_HUD)
381
+ await session.context.addInitScript(getHUDInitScript(session.name ?? session.id));
382
+ if (LEAP_AUTO_CONSENT)
383
+ await session.context.addInitScript(getConsentDismissScript());
384
+ await session.context.addInitScript(getDetectionInitScript());
385
+ if (LEAP_TRACE)
386
+ await session.context.tracing.start({ screenshots: true, snapshots: true });
387
+ // Claim tile slot (without triggering reflow yet — watcher handles it)
388
+ if (tilesCoord) {
389
+ const detected = tileManager.getScreenSize();
390
+ if (detected)
391
+ await tilesCoord.updateScreenSize(detected.width, detected.height).catch(() => { });
392
+ await tilesCoord.claimSlot(session.id).catch(() => { });
393
+ }
394
+ return { session, spec };
395
+ }
396
+ catch (e) {
397
+ return { error: e.message, spec };
398
+ }
399
+ });
400
+ const created = await Promise.all(createPromises);
401
+ // Phase 2: Navigate sessions that have URLs (concurrently)
402
+ const navPromises = created.map(async (result) => {
403
+ if ("error" in result && result.error) {
404
+ results.push({ id: "(failed)", url: result.spec.url, error: result.error });
405
+ return;
406
+ }
407
+ const { session, spec } = result;
408
+ if (spec.url) {
409
+ try {
410
+ const parsed = new URL(spec.url);
411
+ if (!["http:", "https:"].includes(parsed.protocol)) {
412
+ results.push({ id: session.id, error: `Blocked URL scheme: ${parsed.protocol}` });
413
+ return;
414
+ }
415
+ const ssrfBlock = await checkSSRF(parsed.hostname);
416
+ if (ssrfBlock) {
417
+ results.push({ id: session.id, error: ssrfBlock });
418
+ return;
419
+ }
420
+ await session.page.goto(spec.url, {
421
+ waitUntil: spec.waitUntil || "load",
422
+ timeout: 30000,
423
+ });
424
+ results.push({ id: session.id, url: spec.url });
425
+ }
426
+ catch (e) {
427
+ results.push({ id: session.id, url: spec.url, error: e.message });
428
+ }
429
+ }
430
+ else {
431
+ results.push({ id: session.id });
432
+ }
433
+ });
434
+ await Promise.all(navPromises);
435
+ // Phase 3: Single global reflow
436
+ await reflowWithContext().catch(() => { });
437
+ const stats = sessions.getStats();
438
+ const lines = results.map((r) => r.error
439
+ ? `${r.id} — ERROR: ${r.error}`
440
+ : `${r.id}${r.url ? ` → ${r.url}` : ""}`);
441
+ return ok(`Created ${results.filter((r) => !r.error).length}/${sessionSpecs.length} sessions\n` +
442
+ `Pool: ${stats.active}/${stats.maxSessions} active\n\n` +
443
+ lines.join("\n"));
444
+ }
445
+ catch (e) {
446
+ return err(e.message);
447
+ }
448
+ });
350
449
  // ─── session_list ───────────────────────────────────────────────────────────
351
450
  server.registerTool("session_list", {
352
451
  title: "List Browser Sessions",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leapfrog-mcp",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Multi-session browser MCP for AI agents — 36 tools, stealth, persistent auth, code-first scripts, API sniffer, agent intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",