@everydaydevopsio/pilot 0.6.2 → 0.7.0

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 (107) hide show
  1. package/AGENTS.md +11 -0
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +105 -11
  4. package/dist/browser/browser-manager.d.ts +7 -1
  5. package/dist/browser/browser-manager.d.ts.map +1 -1
  6. package/dist/browser/browser-manager.js +196 -40
  7. package/dist/browser/browser-manager.js.map +1 -1
  8. package/dist/browser/chrome-launcher.d.ts +13 -2
  9. package/dist/browser/chrome-launcher.d.ts.map +1 -1
  10. package/dist/browser/chrome-launcher.js +33 -4
  11. package/dist/browser/chrome-launcher.js.map +1 -1
  12. package/dist/browser/driver/capabilities.d.ts +22 -0
  13. package/dist/browser/driver/capabilities.d.ts.map +1 -0
  14. package/dist/browser/driver/capabilities.js +70 -0
  15. package/dist/browser/driver/capabilities.js.map +1 -0
  16. package/dist/browser/driver/cdp-driver.d.ts +77 -0
  17. package/dist/browser/driver/cdp-driver.d.ts.map +1 -0
  18. package/dist/browser/driver/cdp-driver.js +190 -0
  19. package/dist/browser/driver/cdp-driver.js.map +1 -0
  20. package/dist/browser/driver/factory.d.ts +10 -0
  21. package/dist/browser/driver/factory.d.ts.map +1 -0
  22. package/dist/browser/driver/factory.js +23 -0
  23. package/dist/browser/driver/factory.js.map +1 -0
  24. package/dist/browser/driver/index.d.ts +6 -0
  25. package/dist/browser/driver/index.d.ts.map +1 -0
  26. package/dist/browser/driver/index.js +5 -0
  27. package/dist/browser/driver/index.js.map +1 -0
  28. package/dist/browser/driver/types.d.ts +106 -0
  29. package/dist/browser/driver/types.d.ts.map +1 -0
  30. package/dist/browser/driver/types.js +25 -0
  31. package/dist/browser/driver/types.js.map +1 -0
  32. package/dist/browser/sessions.d.ts +49 -0
  33. package/dist/browser/sessions.d.ts.map +1 -0
  34. package/dist/browser/sessions.js +249 -0
  35. package/dist/browser/sessions.js.map +1 -0
  36. package/dist/browser/types.d.ts +13 -0
  37. package/dist/browser/types.d.ts.map +1 -1
  38. package/dist/cli/skill-template.d.ts +1 -1
  39. package/dist/cli/skill-template.d.ts.map +1 -1
  40. package/dist/cli/skill-template.js +1 -1
  41. package/dist/cli/skill-template.js.map +1 -1
  42. package/dist/commands/connect.d.ts +2 -2
  43. package/dist/commands/connect.d.ts.map +1 -1
  44. package/dist/commands/connect.js +2 -2
  45. package/dist/commands/connect.js.map +1 -1
  46. package/dist/mcp/server.d.ts +9 -15
  47. package/dist/mcp/server.d.ts.map +1 -1
  48. package/dist/mcp/server.js +32 -74
  49. package/dist/mcp/server.js.map +1 -1
  50. package/dist/mcp/tools/browser.d.ts +2 -3
  51. package/dist/mcp/tools/browser.d.ts.map +1 -1
  52. package/dist/mcp/tools/browser.js +85 -70
  53. package/dist/mcp/tools/browser.js.map +1 -1
  54. package/dist/mcp/tools/connect.d.ts +2 -3
  55. package/dist/mcp/tools/connect.d.ts.map +1 -1
  56. package/dist/mcp/tools/connect.js +9 -9
  57. package/dist/mcp/tools/connect.js.map +1 -1
  58. package/dist/mcp/tools/driver-context.d.ts +14 -0
  59. package/dist/mcp/tools/driver-context.d.ts.map +1 -0
  60. package/dist/mcp/tools/driver-context.js +43 -0
  61. package/dist/mcp/tools/driver-context.js.map +1 -0
  62. package/dist/mcp/tools/files.d.ts.map +1 -1
  63. package/dist/mcp/tools/files.js +5 -15
  64. package/dist/mcp/tools/files.js.map +1 -1
  65. package/dist/mcp/tools/interaction.d.ts.map +1 -1
  66. package/dist/mcp/tools/interaction.js +7 -26
  67. package/dist/mcp/tools/interaction.js.map +1 -1
  68. package/dist/mcp/tools/names.d.ts +1 -1
  69. package/dist/mcp/tools/names.d.ts.map +1 -1
  70. package/dist/mcp/tools/names.js +2 -0
  71. package/dist/mcp/tools/names.js.map +1 -1
  72. package/dist/mcp/tools/network.d.ts.map +1 -1
  73. package/dist/mcp/tools/network.js +3 -11
  74. package/dist/mcp/tools/network.js.map +1 -1
  75. package/dist/mcp/tools/performance.d.ts.map +1 -1
  76. package/dist/mcp/tools/performance.js +2 -12
  77. package/dist/mcp/tools/performance.js.map +1 -1
  78. package/dist/mcp/tools/snapshot.d.ts.map +1 -1
  79. package/dist/mcp/tools/snapshot.js +3 -14
  80. package/dist/mcp/tools/snapshot.js.map +1 -1
  81. package/dist/mcp/tools/styles.d.ts.map +1 -1
  82. package/dist/mcp/tools/styles.js +2 -11
  83. package/dist/mcp/tools/styles.js.map +1 -1
  84. package/package.json +1 -1
  85. package/skills/pilot/SKILL.md +9 -0
  86. package/src/browser/browser-manager.ts +248 -41
  87. package/src/browser/chrome-launcher.ts +40 -4
  88. package/src/browser/driver/capabilities.ts +88 -0
  89. package/src/browser/driver/cdp-driver.ts +260 -0
  90. package/src/browser/driver/factory.ts +33 -0
  91. package/src/browser/driver/index.ts +22 -0
  92. package/src/browser/driver/types.ts +156 -0
  93. package/src/browser/sessions.ts +299 -0
  94. package/src/browser/types.ts +14 -0
  95. package/src/cli/skill-template.ts +1 -1
  96. package/src/commands/connect.ts +3 -3
  97. package/src/mcp/server.ts +48 -91
  98. package/src/mcp/tools/browser.ts +105 -87
  99. package/src/mcp/tools/connect.ts +10 -11
  100. package/src/mcp/tools/driver-context.ts +56 -0
  101. package/src/mcp/tools/files.ts +5 -16
  102. package/src/mcp/tools/interaction.ts +7 -27
  103. package/src/mcp/tools/names.ts +2 -0
  104. package/src/mcp/tools/network.ts +2 -12
  105. package/src/mcp/tools/performance.ts +2 -13
  106. package/src/mcp/tools/snapshot.ts +3 -15
  107. package/src/mcp/tools/styles.ts +2 -12
@@ -1,26 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import type { Client } from 'chrome-remote-interface';
4
- import type { BrowserManager } from '../../browser.js';
5
- import type { BrowserContext } from '../server.js';
6
- import { executeScreenshot } from '../../commands/screenshot.js';
7
- import { executeNavigate } from '../../commands/navigate.js';
8
- import { executeClick } from '../../commands/click.js';
9
- import { executeType } from '../../commands/type.js';
10
- import { executeEvaluate } from '../../commands/evaluate.js';
11
- import { executeWait } from '../../commands/wait.js';
12
- import { executePageInfo } from '../../commands/page_info.js';
3
+ import type { BrowserContext, DriverFactory } from '../server.js';
4
+ import { DEFAULT_STARTUP_TIMEOUT_MS } from '../../browser/chrome-launcher.js';
13
5
  import {
14
- executeListTabs,
15
- executeNewTab,
16
- executeCloseTab,
17
- executeSwitchTab
18
- } from '../../commands/tabs.js';
19
- import {
20
- VIEWPORT_PRESETS,
21
- applyViewport,
22
- type ViewportConfig
23
- } from '../../viewport.js';
6
+ listProfiles,
7
+ pilotDataDir,
8
+ stopPilotBrowsers
9
+ } from '../../browser/sessions.js';
10
+ import { VIEWPORT_PRESETS } from '../../viewport.js';
11
+ import { requireDriver } from './driver-context.js';
24
12
 
25
13
  // Raw shapes for MCP SDK (expects ZodRawShape, not ZodObject)
26
14
 
@@ -185,9 +173,9 @@ const startShape = {
185
173
  .number()
186
174
  .int()
187
175
  .positive()
188
- .default(30000)
176
+ .default(DEFAULT_STARTUP_TIMEOUT_MS)
189
177
  .describe(
190
- 'Timeout waiting for Chrome DevTools to become ready, in milliseconds.'
178
+ `Timeout waiting for Chrome DevTools to become ready, in milliseconds. Defaults to ${DEFAULT_STARTUP_TIMEOUT_MS}. Startup fails immediately if Chrome exits, so raising this only helps when Chrome is slow to start (for example a first run on a Linux server with software rendering).`
191
179
  )
192
180
  };
193
181
 
@@ -242,22 +230,10 @@ const viewportResizeShape = {
242
230
  )
243
231
  };
244
232
 
245
- function requireClient(context: BrowserContext): {
246
- client: Client;
247
- manager: BrowserManager;
248
- } {
249
- const manager = context.manager;
250
- if (!manager || !manager.isConnected()) {
251
- throw new Error('Browser not started. Call browser_start first.');
252
- }
253
- const client = manager.getClient()!;
254
- return { client, manager };
255
- }
256
-
257
233
  export function registerBrowserTools(
258
234
  server: McpServer,
259
235
  context: BrowserContext,
260
- makeBrowserManager: () => BrowserManager
236
+ makeDriver: DriverFactory
261
237
  ): void {
262
238
  server.tool(
263
239
  'browser_start',
@@ -274,7 +250,7 @@ export function registerBrowserTools(
274
250
  responsive,
275
251
  startupTimeoutMs
276
252
  }) => {
277
- if (context.manager?.isConnected()) {
253
+ if (context.driver?.isConnected()) {
278
254
  return {
279
255
  content: [
280
256
  {
@@ -285,14 +261,14 @@ export function registerBrowserTools(
285
261
  };
286
262
  }
287
263
 
288
- if (context.manager) {
289
- await context.manager.destroy();
290
- context.manager = null;
264
+ if (context.driver) {
265
+ await context.driver.destroy();
266
+ context.driver = null;
291
267
  }
292
268
 
293
- const manager = makeBrowserManager();
269
+ const driver = makeDriver();
294
270
  const { headless: effectiveHeadless, viewport: effectiveViewport } =
295
- await manager.launch({
271
+ await driver.launch({
296
272
  headless,
297
273
  chromePath,
298
274
  profileName,
@@ -303,7 +279,7 @@ export function registerBrowserTools(
303
279
  responsive,
304
280
  startupTimeoutMs
305
281
  });
306
- context.manager = manager;
282
+ context.driver = driver;
307
283
 
308
284
  return {
309
285
  content: [
@@ -321,14 +297,19 @@ export function registerBrowserTools(
321
297
  'Stop the Chrome browser and free all resources.',
322
298
  {},
323
299
  async () => {
324
- if (!context.manager) {
300
+ if (!context.driver) {
325
301
  return {
326
- content: [{ type: 'text' as const, text: 'Browser is not running.' }]
302
+ content: [
303
+ {
304
+ type: 'text' as const,
305
+ text: 'Browser is not running. This Pilot session is not tracking a launched or connected browser; a failed browser_start stops the Chrome it launched and reports the outcome in its error.'
306
+ }
307
+ ]
327
308
  };
328
309
  }
329
310
 
330
- await context.manager.destroy();
331
- context.manager = null;
311
+ await context.driver.destroy();
312
+ context.driver = null;
332
313
 
333
314
  return {
334
315
  content: [{ type: 'text' as const, text: 'Browser stopped.' }]
@@ -336,13 +317,70 @@ export function registerBrowserTools(
336
317
  }
337
318
  );
338
319
 
320
+ server.tool(
321
+ 'browser_status',
322
+ 'Show the browser tracked by this Pilot session and the lock state of every Pilot profile on this machine (including browsers started by other Pilot sessions). Read-only. Use it to diagnose "profile already in use" errors.',
323
+ {},
324
+ async () => {
325
+ const status = {
326
+ session: context.driver?.getSessionInfo() ?? {
327
+ mode: 'none',
328
+ connected: false
329
+ },
330
+ dataDir: pilotDataDir(),
331
+ profiles: listProfiles()
332
+ };
333
+ return {
334
+ content: [
335
+ { type: 'text' as const, text: JSON.stringify(status, null, 2) }
336
+ ]
337
+ };
338
+ }
339
+ );
340
+
341
+ server.tool(
342
+ 'browser_stop_all',
343
+ 'Stop every Pilot-managed Chrome on this machine: the browser in this session plus any Chrome holding a Pilot profile lock, including ones started by other Pilot sessions. Only processes whose command line shows Chrome using a Pilot profile directory are stopped; browsers on other hosts and unverifiable locks are skipped. Also removes stale locks. Ask the user before calling this, since it can close browsers other agents are using.',
344
+ {},
345
+ async () => {
346
+ const lines: string[] = [];
347
+ if (context.driver) {
348
+ const info = context.driver.getSessionInfo();
349
+ await context.driver.destroy();
350
+ context.driver = null;
351
+ if (info.mode === 'launched') {
352
+ lines.push(
353
+ `this session: stopped Chrome pid ${info.pid} (profile "${info.profile}")`
354
+ );
355
+ } else if (info.mode === 'external') {
356
+ lines.push(
357
+ `this session: disconnected from ${info.host}:${info.cdpPort} (not stopped; Pilot did not launch it)`
358
+ );
359
+ }
360
+ }
361
+ for (const result of await stopPilotBrowsers()) {
362
+ lines.push(`${result.profile}: ${result.outcome} - ${result.detail}`);
363
+ }
364
+ return {
365
+ content: [
366
+ {
367
+ type: 'text' as const,
368
+ text:
369
+ lines.length > 0
370
+ ? lines.join('\n')
371
+ : 'No Pilot-managed browsers were running.'
372
+ }
373
+ ]
374
+ };
375
+ }
376
+ );
377
+
339
378
  server.tool(
340
379
  'browser_screenshot',
341
380
  'Capture a screenshot of the current browser viewport or full page',
342
381
  screenshotShape,
343
382
  async ({ format, quality, fullPage }) => {
344
- const { client } = requireClient(context);
345
- const result = await executeScreenshot(client, {
383
+ const result = await requireDriver(context).screenshot({
346
384
  format,
347
385
  quality,
348
386
  fullPage
@@ -377,8 +415,7 @@ export function registerBrowserTools(
377
415
  'Navigate to a URL and wait for page load',
378
416
  navigateShape,
379
417
  async ({ url, waitUntil, timeoutMs }) => {
380
- const { client, manager } = requireClient(context);
381
- const result = await executeNavigate(client, manager, {
418
+ const result = await requireDriver(context).navigate({
382
419
  url,
383
420
  waitUntil,
384
421
  timeoutMs
@@ -416,8 +453,7 @@ export function registerBrowserTools(
416
453
  };
417
454
  }
418
455
 
419
- const { client } = requireClient(context);
420
- const result = await executeClick(client, params, context.elementRefMap);
456
+ const result = await requireDriver(context).click(params);
421
457
 
422
458
  return {
423
459
  content: [
@@ -435,8 +471,7 @@ export function registerBrowserTools(
435
471
  'Type text into the focused element, a ref (from browser_snapshot), or a CSS selector. Generates keystroke events (use browser_fill to replace a field value entirely).',
436
472
  typeShape,
437
473
  async (params) => {
438
- const { client } = requireClient(context);
439
- await executeType(client, params, context.elementRefMap);
474
+ await requireDriver(context).type(params);
440
475
 
441
476
  const target = params.ref ?? params.selector;
442
477
  return {
@@ -455,8 +490,7 @@ export function registerBrowserTools(
455
490
  'Execute JavaScript in the page context and return the result',
456
491
  evaluateShape,
457
492
  async ({ expression, awaitPromise, timeoutMs }) => {
458
- const { client } = requireClient(context);
459
- const result = await executeEvaluate(client, {
493
+ const result = await requireDriver(context).evaluate({
460
494
  expression,
461
495
  awaitPromise,
462
496
  timeoutMs
@@ -499,8 +533,7 @@ export function registerBrowserTools(
499
533
  };
500
534
  }
501
535
 
502
- const { client, manager } = requireClient(context);
503
- const result = await executeWait(client, manager, params);
536
+ const result = await requireDriver(context).wait(params);
504
537
 
505
538
  return {
506
539
  content: [
@@ -518,8 +551,7 @@ export function registerBrowserTools(
518
551
  'Get information about the current page (URL, title, ready state)',
519
552
  {},
520
553
  async () => {
521
- const { client } = requireClient(context);
522
- const result = await executePageInfo(client);
554
+ const result = await requireDriver(context).pageInfo();
523
555
 
524
556
  return {
525
557
  content: [
@@ -537,8 +569,7 @@ export function registerBrowserTools(
537
569
  'List all open browser tabs with their target IDs, URLs, titles, and active status',
538
570
  {},
539
571
  async () => {
540
- const { manager } = requireClient(context);
541
- const tabs = await executeListTabs(manager);
572
+ const tabs = await requireDriver(context).listTabs();
542
573
 
543
574
  return {
544
575
  content: [
@@ -556,8 +587,7 @@ export function registerBrowserTools(
556
587
  'Open a new browser tab, optionally navigating to a URL',
557
588
  newTabShape,
558
589
  async ({ url }) => {
559
- const { manager } = requireClient(context);
560
- const result = await executeNewTab(manager, url);
590
+ const result = await requireDriver(context).newTab(url);
561
591
 
562
592
  return {
563
593
  content: [
@@ -575,8 +605,7 @@ export function registerBrowserTools(
575
605
  'Close a browser tab by its target ID. Cannot close the currently active tab.',
576
606
  closeTabShape,
577
607
  async ({ targetId }) => {
578
- const { manager } = requireClient(context);
579
- await executeCloseTab(manager, targetId);
608
+ await requireDriver(context).closeTab(targetId);
580
609
 
581
610
  return {
582
611
  content: [
@@ -594,8 +623,7 @@ export function registerBrowserTools(
594
623
  'Switch the active browser tab to the one with the given target ID. All subsequent commands will target this tab.',
595
624
  switchTabShape,
596
625
  async ({ targetId }) => {
597
- const { manager } = requireClient(context);
598
- await executeSwitchTab(manager, targetId);
626
+ await requireDriver(context).switchTab(targetId);
599
627
 
600
628
  return {
601
629
  content: [
@@ -613,22 +641,12 @@ export function registerBrowserTools(
613
641
  'Resize the browser viewport to new dimensions. Updates both the window size and the rendering viewport. Disables responsive mode so the viewport stays locked at the specified size.',
614
642
  viewportResizeShape,
615
643
  async ({ width, height, deviceScaleFactor, mobile }) => {
616
- const { client, manager } = requireClient(context);
617
-
618
- const current = manager.getViewportConfig();
619
- const effectiveMobile = mobile ?? current?.mobile ?? false;
620
- const newConfig: ViewportConfig = {
644
+ const newConfig = await requireDriver(context).resizeViewport({
621
645
  width,
622
646
  height,
623
- deviceScaleFactor: deviceScaleFactor ?? current?.deviceScaleFactor ?? 1,
624
- mobile: effectiveMobile,
625
- responsive: false,
626
- ...(effectiveMobile &&
627
- current?.userAgent && { userAgent: current.userAgent })
628
- };
629
-
630
- await applyViewport(client, newConfig);
631
- manager.setViewportConfig(newConfig);
647
+ deviceScaleFactor,
648
+ mobile
649
+ });
632
650
 
633
651
  return {
634
652
  content: [
@@ -646,11 +664,11 @@ export function registerBrowserTools(
646
664
  'Manage browser tabs. Actions: "list" shows all tabs, "new" opens a tab (optional url), "select" switches to a tab (requires targetId), "close" closes a tab (requires targetId).',
647
665
  tabsShape,
648
666
  async ({ action, targetId, url }) => {
649
- const { manager } = requireClient(context);
667
+ const driver = requireDriver(context);
650
668
 
651
669
  switch (action) {
652
670
  case 'list': {
653
- const tabs = await executeListTabs(manager);
671
+ const tabs = await driver.listTabs();
654
672
  return {
655
673
  content: [
656
674
  { type: 'text' as const, text: JSON.stringify(tabs, null, 2) }
@@ -659,7 +677,7 @@ export function registerBrowserTools(
659
677
  }
660
678
 
661
679
  case 'new': {
662
- const result = await executeNewTab(manager, url);
680
+ const result = await driver.newTab(url);
663
681
  return {
664
682
  content: [
665
683
  {
@@ -682,7 +700,7 @@ export function registerBrowserTools(
682
700
  isError: true
683
701
  };
684
702
  }
685
- await executeSwitchTab(manager, targetId);
703
+ await driver.switchTab(targetId);
686
704
  return {
687
705
  content: [
688
706
  {
@@ -705,7 +723,7 @@ export function registerBrowserTools(
705
723
  isError: true
706
724
  };
707
725
  }
708
- await executeCloseTab(manager, targetId);
726
+ await driver.closeTab(targetId);
709
727
  return {
710
728
  content: [
711
729
  {
@@ -1,7 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import type { BrowserContext } from '../server.js';
4
- import type { BrowserManager } from '../../browser/browser-manager.js';
3
+ import type { BrowserContext, DriverFactory } from '../server.js';
5
4
  import { executeConnect, formatConnectResult } from '../../commands/connect.js';
6
5
 
7
6
  const connectShape = {
@@ -21,14 +20,14 @@ const connectShape = {
21
20
  export function registerConnectTools(
22
21
  server: McpServer,
23
22
  context: BrowserContext,
24
- makeBrowserManager: () => BrowserManager
23
+ makeDriver: DriverFactory
25
24
  ): void {
26
25
  server.tool(
27
26
  'browser_connect',
28
27
  'Connect to an existing Chrome instance with remote debugging enabled. Start Chrome with --remote-debugging-port=PORT, then use this tool to connect. Lists available tabs after connecting. Use browser_switch_tab to select a specific tab.',
29
28
  connectShape,
30
29
  async ({ host, port }) => {
31
- if (context.manager?.isConnected()) {
30
+ if (context.driver?.isConnected()) {
32
31
  return {
33
32
  content: [
34
33
  {
@@ -39,22 +38,22 @@ export function registerConnectTools(
39
38
  };
40
39
  }
41
40
 
42
- if (context.manager) {
43
- await context.manager.destroy();
44
- context.manager = null;
41
+ if (context.driver) {
42
+ await context.driver.destroy();
43
+ context.driver = null;
45
44
  }
46
45
 
47
- const manager = makeBrowserManager();
46
+ const driver = makeDriver();
48
47
  try {
49
- const result = await executeConnect(manager, { host, port });
50
- context.manager = manager;
48
+ const result = await executeConnect(driver, { host, port });
49
+ context.driver = driver;
51
50
  return {
52
51
  content: [
53
52
  { type: 'text' as const, text: formatConnectResult(result) }
54
53
  ]
55
54
  };
56
55
  } catch (err) {
57
- await manager.destroy();
56
+ await driver.destroy();
58
57
  throw err;
59
58
  }
60
59
  }
@@ -0,0 +1,56 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import {
3
+ TOOL_CAPABILITIES,
4
+ assertToolSupported
5
+ } from '../../browser/driver/capabilities.js';
6
+ import type { BrowserDriver } from '../../browser/driver/types.js';
7
+ import type { ToolName } from './names.js';
8
+
9
+ /** Return the connected driver or fail with the standard "not started" error. */
10
+ export function requireDriver(context: {
11
+ driver: BrowserDriver | null;
12
+ }): BrowserDriver {
13
+ const driver = context.driver;
14
+ if (!driver || !driver.isConnected()) {
15
+ throw new Error('Browser not started. Call browser_start first.');
16
+ }
17
+ return driver;
18
+ }
19
+
20
+ function isToolName(name: string): name is ToolName {
21
+ return Object.prototype.hasOwnProperty.call(TOOL_CAPABILITIES, name);
22
+ }
23
+
24
+ /**
25
+ * Wrap an McpServer so every registered tool first checks that the active
26
+ * driver (if any) has the capabilities the tool declares in TOOL_CAPABILITIES.
27
+ * Registering a tool without a capability declaration throws, so new tools
28
+ * cannot skip the check.
29
+ */
30
+ export function withCapabilityGate(
31
+ server: McpServer,
32
+ getDriver: () => BrowserDriver | null
33
+ ): McpServer {
34
+ return new Proxy(server, {
35
+ get(target, prop, receiver) {
36
+ if (prop !== 'tool') return Reflect.get(target, prop, receiver);
37
+ return (name: string, ...rest: unknown[]) => {
38
+ if (!isToolName(name)) {
39
+ throw new Error(
40
+ `Tool ${name} has no entry in TOOL_CAPABILITIES; declare the driver capabilities it needs.`
41
+ );
42
+ }
43
+ const last = rest.length - 1;
44
+ const handler = rest[last];
45
+ if (typeof handler === 'function') {
46
+ rest[last] = async (...args: unknown[]) => {
47
+ const driver = getDriver();
48
+ if (driver) assertToolSupported(name, driver);
49
+ return handler(...args);
50
+ };
51
+ }
52
+ return (target.tool as (...a: unknown[]) => unknown)(name, ...rest);
53
+ };
54
+ }
55
+ });
56
+ }
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import type { BrowserContext } from '../server.js';
4
- import { executeDialog, formatDialogResult } from '../../commands/dialog.js';
5
- import { executeUpload } from '../../commands/upload.js';
4
+ import { formatDialogResult } from '../../commands/dialog.js';
5
+ import { requireDriver } from './driver-context.js';
6
6
  import {
7
7
  executeDownloads,
8
8
  formatDownloadsResult
@@ -49,15 +49,6 @@ const downloadsShape = {
49
49
  .describe('Timeout for wait action in milliseconds (default: 30000)')
50
50
  };
51
51
 
52
- function requireContext(context: BrowserContext) {
53
- const manager = context.manager;
54
- if (!manager || !manager.isConnected()) {
55
- throw new Error('Browser not started. Call browser_start first.');
56
- }
57
- const client = manager.getClient()!;
58
- return { client };
59
- }
60
-
61
52
  export function registerFilesTools(
62
53
  server: McpServer,
63
54
  context: BrowserContext
@@ -67,8 +58,7 @@ export function registerFilesTools(
67
58
  'Handle browser dialogs (alert, confirm, prompt). Use "list" to see pending dialogs, "accept" to click OK/Yes, or "dismiss" to click Cancel/No. For prompt dialogs, provide promptText with accept.',
68
59
  dialogShape,
69
60
  async ({ action, promptText }) => {
70
- const { client } = requireContext(context);
71
- const result = await executeDialog(client, context.dialogQueue, {
61
+ const result = await requireDriver(context).dialog({
72
62
  action,
73
63
  promptText
74
64
  });
@@ -83,8 +73,7 @@ export function registerFilesTools(
83
73
  'Upload files to a file input element by ref. Files must exist, be regular files, and be under allowed roots (PILOT_UPLOAD_ROOTS env, default: cwd). Use browser_snapshot to find file input refs.',
84
74
  uploadShape,
85
75
  async ({ ref, files }) => {
86
- const { client } = requireContext(context);
87
- const result = await executeUpload(client, context.elementRefMap, {
76
+ const result = await requireDriver(context).upload({
88
77
  ref,
89
78
  files
90
79
  });
@@ -104,7 +93,7 @@ export function registerFilesTools(
104
93
  'Track file downloads. Actions: "list" shows tracked downloads with status/progress; "wait" blocks until a download (by GUID) completes; "clear" resets the tracker.',
105
94
  downloadsShape,
106
95
  async ({ action, guid, timeoutMs }) => {
107
- requireContext(context);
96
+ requireDriver(context);
108
97
  const result = await executeDownloads(context.downloadTracker, {
109
98
  action,
110
99
  guid,
@@ -1,21 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import type { BrowserContext } from '../server.js';
4
- import { executeFill } from '../../commands/fill.js';
5
- import { executeHover } from '../../commands/hover.js';
6
- import { executePressKey } from '../../commands/press_key.js';
7
- import { executeSelect } from '../../commands/select.js';
8
- import { executeCheck } from '../../commands/check.js';
9
- import { executeScroll } from '../../commands/scroll.js';
10
-
11
- function requireContext(context: BrowserContext) {
12
- const manager = context.manager;
13
- if (!manager || !manager.isConnected()) {
14
- throw new Error('Browser not started. Call browser_start first.');
15
- }
16
- const client = manager.getClient()!;
17
- return { client, refMap: context.elementRefMap };
18
- }
4
+ import { requireDriver } from './driver-context.js';
19
5
 
20
6
  const fillShape = {
21
7
  ref: z.string().describe('Element ref from browser_snapshot (e.g. "e3")'),
@@ -81,8 +67,7 @@ export function registerInteractionTools(
81
67
  'Fill a form field by element ref. Replaces the entire field value and fires input/change events compatible with React, Vue, and Svelte. Use browser_snapshot first to get refs.',
82
68
  fillShape,
83
69
  async ({ ref, value }) => {
84
- const { client, refMap } = requireContext(context);
85
- const result = await executeFill(client, refMap, { ref, value });
70
+ const result = await requireDriver(context).fill({ ref, value });
86
71
  return {
87
72
  content: [
88
73
  {
@@ -113,8 +98,7 @@ export function registerInteractionTools(
113
98
  isError: true
114
99
  };
115
100
  }
116
- const { client, refMap } = requireContext(context);
117
- const result = await executeHover(client, refMap, params);
101
+ const result = await requireDriver(context).hover(params);
118
102
  return {
119
103
  content: [
120
104
  {
@@ -131,8 +115,7 @@ export function registerInteractionTools(
131
115
  'Press a key or key combination. Supports named keys (Enter, Tab, Escape, etc.) and modifier combos (Control+a, Meta+c, Shift+Enter).',
132
116
  pressKeyShape,
133
117
  async ({ key }) => {
134
- const { client } = requireContext(context);
135
- const result = await executePressKey(client, { key });
118
+ const result = await requireDriver(context).pressKey({ key });
136
119
  const modStr =
137
120
  result.modifiers.length > 0
138
121
  ? ` with modifiers: ${result.modifiers.join('+')}`
@@ -165,8 +148,7 @@ export function registerInteractionTools(
165
148
  isError: true
166
149
  };
167
150
  }
168
- const { client, refMap } = requireContext(context);
169
- const result = await executeSelect(client, refMap, params);
151
+ const result = await requireDriver(context).select(params);
170
152
  return {
171
153
  content: [
172
154
  {
@@ -183,8 +165,7 @@ export function registerInteractionTools(
183
165
  'Toggle a checkbox or radio button by ref. Optionally set to a specific checked state.',
184
166
  checkShape,
185
167
  async ({ ref, checked }) => {
186
- const { client, refMap } = requireContext(context);
187
- const result = await executeCheck(client, refMap, { ref, checked });
168
+ const result = await requireDriver(context).check({ ref, checked });
188
169
  return {
189
170
  content: [
190
171
  {
@@ -217,8 +198,7 @@ export function registerInteractionTools(
217
198
  isError: true
218
199
  };
219
200
  }
220
- const { client, refMap } = requireContext(context);
221
- const result = await executeScroll(client, refMap, params);
201
+ const result = await requireDriver(context).scroll(params);
222
202
  return {
223
203
  content: [
224
204
  {
@@ -5,6 +5,8 @@
5
5
  export const EXPECTED_TOOLS = [
6
6
  'browser_start',
7
7
  'browser_stop',
8
+ 'browser_status',
9
+ 'browser_stop_all',
8
10
  'browser_screenshot',
9
11
  'browser_navigate',
10
12
  'browser_click',
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
  import type { BrowserContext } from '../server.js';
4
+ import { requireDriver } from './driver-context.js';
4
5
  import {
5
- executeNetwork,
6
6
  formatNetworkList,
7
7
  formatNetworkDetail
8
8
  } from '../../commands/network.js';
@@ -48,15 +48,6 @@ const networkShape = {
48
48
  .describe('Maximum number of results (list only)')
49
49
  };
50
50
 
51
- function requireContext(context: BrowserContext) {
52
- const manager = context.manager;
53
- if (!manager || !manager.isConnected()) {
54
- throw new Error('Browser not started. Call browser_start first.');
55
- }
56
- const client = manager.getClient()!;
57
- return { client, networkBuffer: context.networkBuffer };
58
- }
59
-
60
51
  export function registerNetworkTools(
61
52
  server: McpServer,
62
53
  context: BrowserContext
@@ -66,8 +57,7 @@ export function registerNetworkTools(
66
57
  'Inspect captured network requests. Actions: "list" shows request summaries with filters (url, method, status, resourceType, failed); "get" returns full detail for a request ID including headers and response body; "clear" resets the buffer.',
67
58
  networkShape,
68
59
  async (params) => {
69
- const { client, networkBuffer } = requireContext(context);
70
- const result = await executeNetwork(client, networkBuffer, params);
60
+ const result = await requireDriver(context).network(params);
71
61
 
72
62
  let text: string;
73
63
  if (result.action === 'list') {