leapfrog-mcp 0.6.5 → 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.
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",
@@ -577,22 +577,9 @@ export class SessionManager {
577
577
  logger.warn("tile.position_failed", { error: e.message });
578
578
  }
579
579
  }
580
- // ── Debounced reflow after creation (Windows only) ─────────────
581
- // On Windows, launch args position each window for a grid that includes
582
- // only the sessions created so far. Earlier windows end up at stale positions.
583
- // Reflow all after a 500ms debounce so rapid batch creates settle into one reflow.
584
- // Skipped on macOS where CDP reflow can move windows to the wrong screen.
585
- if (process.platform === "win32" && tileManager.isEnabled() && this.sessions.size > 1) {
586
- clearTimeout(globalThis.__leapReflowTimer);
587
- globalThis.__leapReflowTimer = setTimeout(async () => {
588
- try {
589
- await tileManager.reflowAll(this.sessions);
590
- }
591
- catch (e) {
592
- logger.warn("tile.reflow_after_create_failed", { error: e.message });
593
- }
594
- }, 500);
595
- }
580
+ // Reflow is handled by the TilesCoordinator file watcher in index.ts,
581
+ // which calls reflowWithContext() with global multi-terminal grid state.
582
+ // A local-only reflowAll here would override the global grid positions.
596
583
  // ── Auto-reflow on external close ──────────────────────────────
597
584
  // When the user manually closes a browser window (X button), clean up
598
585
  // the session and reflow remaining tiled windows. Without this, closing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leapfrog-mcp",
3
- "version": "0.6.5",
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",