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 +99 -0
- package/dist/session-manager.js +3 -16
- 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/dist/session-manager.js
CHANGED
|
@@ -577,22 +577,9 @@ export class SessionManager {
|
|
|
577
577
|
logger.warn("tile.position_failed", { error: e.message });
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
// only
|
|
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.
|
|
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",
|