@everydaydevopsio/pilot 0.6.3 → 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 (81) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +19 -2
  4. package/dist/browser/driver/capabilities.d.ts +22 -0
  5. package/dist/browser/driver/capabilities.d.ts.map +1 -0
  6. package/dist/browser/driver/capabilities.js +70 -0
  7. package/dist/browser/driver/capabilities.js.map +1 -0
  8. package/dist/browser/driver/cdp-driver.d.ts +77 -0
  9. package/dist/browser/driver/cdp-driver.d.ts.map +1 -0
  10. package/dist/browser/driver/cdp-driver.js +190 -0
  11. package/dist/browser/driver/cdp-driver.js.map +1 -0
  12. package/dist/browser/driver/factory.d.ts +10 -0
  13. package/dist/browser/driver/factory.d.ts.map +1 -0
  14. package/dist/browser/driver/factory.js +23 -0
  15. package/dist/browser/driver/factory.js.map +1 -0
  16. package/dist/browser/driver/index.d.ts +6 -0
  17. package/dist/browser/driver/index.d.ts.map +1 -0
  18. package/dist/browser/driver/index.js +5 -0
  19. package/dist/browser/driver/index.js.map +1 -0
  20. package/dist/browser/driver/types.d.ts +106 -0
  21. package/dist/browser/driver/types.d.ts.map +1 -0
  22. package/dist/browser/driver/types.js +25 -0
  23. package/dist/browser/driver/types.js.map +1 -0
  24. package/dist/browser/types.d.ts +3 -0
  25. package/dist/browser/types.d.ts.map +1 -1
  26. package/dist/commands/connect.d.ts +2 -2
  27. package/dist/commands/connect.d.ts.map +1 -1
  28. package/dist/commands/connect.js +2 -2
  29. package/dist/commands/connect.js.map +1 -1
  30. package/dist/mcp/server.d.ts +9 -15
  31. package/dist/mcp/server.d.ts.map +1 -1
  32. package/dist/mcp/server.js +32 -74
  33. package/dist/mcp/server.js.map +1 -1
  34. package/dist/mcp/tools/browser.d.ts +2 -3
  35. package/dist/mcp/tools/browser.d.ts.map +1 -1
  36. package/dist/mcp/tools/browser.js +38 -72
  37. package/dist/mcp/tools/browser.js.map +1 -1
  38. package/dist/mcp/tools/connect.d.ts +2 -3
  39. package/dist/mcp/tools/connect.d.ts.map +1 -1
  40. package/dist/mcp/tools/connect.js +9 -9
  41. package/dist/mcp/tools/connect.js.map +1 -1
  42. package/dist/mcp/tools/driver-context.d.ts +14 -0
  43. package/dist/mcp/tools/driver-context.d.ts.map +1 -0
  44. package/dist/mcp/tools/driver-context.js +43 -0
  45. package/dist/mcp/tools/driver-context.js.map +1 -0
  46. package/dist/mcp/tools/files.d.ts.map +1 -1
  47. package/dist/mcp/tools/files.js +5 -15
  48. package/dist/mcp/tools/files.js.map +1 -1
  49. package/dist/mcp/tools/interaction.d.ts.map +1 -1
  50. package/dist/mcp/tools/interaction.js +7 -26
  51. package/dist/mcp/tools/interaction.js.map +1 -1
  52. package/dist/mcp/tools/network.d.ts.map +1 -1
  53. package/dist/mcp/tools/network.js +3 -11
  54. package/dist/mcp/tools/network.js.map +1 -1
  55. package/dist/mcp/tools/performance.d.ts.map +1 -1
  56. package/dist/mcp/tools/performance.js +2 -12
  57. package/dist/mcp/tools/performance.js.map +1 -1
  58. package/dist/mcp/tools/snapshot.d.ts.map +1 -1
  59. package/dist/mcp/tools/snapshot.js +3 -14
  60. package/dist/mcp/tools/snapshot.js.map +1 -1
  61. package/dist/mcp/tools/styles.d.ts.map +1 -1
  62. package/dist/mcp/tools/styles.js +2 -11
  63. package/dist/mcp/tools/styles.js.map +1 -1
  64. package/package.json +1 -1
  65. package/src/browser/driver/capabilities.ts +88 -0
  66. package/src/browser/driver/cdp-driver.ts +260 -0
  67. package/src/browser/driver/factory.ts +33 -0
  68. package/src/browser/driver/index.ts +22 -0
  69. package/src/browser/driver/types.ts +156 -0
  70. package/src/browser/types.ts +3 -0
  71. package/src/commands/connect.ts +3 -3
  72. package/src/mcp/server.ts +48 -91
  73. package/src/mcp/tools/browser.ts +39 -90
  74. package/src/mcp/tools/connect.ts +10 -11
  75. package/src/mcp/tools/driver-context.ts +56 -0
  76. package/src/mcp/tools/files.ts +5 -16
  77. package/src/mcp/tools/interaction.ts +7 -27
  78. package/src/mcp/tools/network.ts +2 -12
  79. package/src/mcp/tools/performance.ts +2 -13
  80. package/src/mcp/tools/snapshot.ts +3 -15
  81. package/src/mcp/tools/styles.ts +2 -12
package/src/mcp/server.ts CHANGED
@@ -1,18 +1,12 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { BrowserManager } from '../browser.js';
2
+ import { createDriver } from '../browser/driver/factory.js';
3
+ import type { BrowserDriver, DriverSession } from '../browser/driver/types.js';
3
4
  import { ElementRefMap } from '../browser/inspect/element-ref.js';
4
- import { resetCssState } from '../browser/inspect/styles.js';
5
5
  import { DialogQueue } from '../browser/interaction/dialogs.js';
6
- import { attachDialogHandler } from '../browser/interaction/dialogs.js';
7
- import {
8
- DownloadTracker,
9
- enableDownloads,
10
- attachDownloadHandler
11
- } from '../browser/interaction/downloads.js';
6
+ import { DownloadTracker } from '../browser/interaction/downloads.js';
12
7
  import { NetworkBuffer } from '../browser/network/network-buffer.js';
13
- import { attachNetworkMonitor } from '../browser/network/network-monitor.js';
14
8
  import { loadConfig } from '../util/config.js';
15
- import { ConsoleBuffer, type ConsoleMessage } from './console-buffer.js';
9
+ import { ConsoleBuffer } from './console-buffer.js';
16
10
  import { registerBrowserTools } from './tools/browser.js';
17
11
  import { registerErrorTools } from './tools/errors.js';
18
12
  import { registerSnapshotTools } from './tools/snapshot.js';
@@ -24,6 +18,7 @@ import { registerFilesTools } from './tools/files.js';
24
18
  import { createTraceState } from '../browser/performance/trace.js';
25
19
  import { registerPerformanceTools } from './tools/performance.js';
26
20
  import { registerConnectTools } from './tools/connect.js';
21
+ import { withCapabilityGate } from './tools/driver-context.js';
27
22
 
28
23
  export interface McpConfig {
29
24
  bufferSize: number;
@@ -31,17 +26,35 @@ export interface McpConfig {
31
26
  cdpHost?: string;
32
27
  }
33
28
 
34
- export interface BrowserContext {
35
- manager: BrowserManager | null;
36
- consoleBuffer: ConsoleBuffer;
37
- elementRefMap: ElementRefMap;
38
- networkBuffer: NetworkBuffer;
39
- dialogQueue: DialogQueue;
40
- downloadTracker: DownloadTracker;
41
- traceState: ReturnType<typeof createTraceState>;
29
+ export interface BrowserContext extends DriverSession {
30
+ driver: BrowserDriver | null;
42
31
  baseConfig: McpConfig;
43
32
  }
44
33
 
34
+ export type DriverFactory = () => BrowserDriver;
35
+
36
+ /**
37
+ * Register every Pilot tool behind the capability gate, so a tool the active
38
+ * driver cannot support fails before its handler runs.
39
+ */
40
+ export function registerAllTools(
41
+ server: McpServer,
42
+ context: BrowserContext,
43
+ makeDriver: DriverFactory
44
+ ): void {
45
+ const gated = withCapabilityGate(server, () => context.driver);
46
+ registerBrowserTools(gated, context, makeDriver);
47
+ registerErrorTools(gated, context.consoleBuffer);
48
+ registerSnapshotTools(gated, context);
49
+ registerInteractionTools(gated, context);
50
+ registerNetworkTools(gated, context);
51
+ registerRuntimeTools(gated, context.consoleBuffer);
52
+ registerStylesTools(gated, context);
53
+ registerFilesTools(gated, context);
54
+ registerPerformanceTools(gated, context);
55
+ registerConnectTools(gated, context, makeDriver);
56
+ }
57
+
45
58
  export async function createMcpServer(config: McpConfig): Promise<{
46
59
  server: McpServer;
47
60
  cleanup: () => Promise<void>;
@@ -51,88 +64,32 @@ export async function createMcpServer(config: McpConfig): Promise<{
51
64
  version: '0.1.0'
52
65
  });
53
66
 
54
- const consoleBuffer = new ConsoleBuffer(config.bufferSize);
55
- const elementRefMap = new ElementRefMap();
56
- const networkBuffer = new NetworkBuffer();
57
- const dialogQueue = new DialogQueue();
58
- const downloadTracker = new DownloadTracker();
59
- const traceState = createTraceState();
60
-
61
67
  const context: BrowserContext = {
62
- manager: null,
63
- consoleBuffer,
64
- elementRefMap,
65
- networkBuffer,
66
- dialogQueue,
67
- downloadTracker,
68
- traceState,
68
+ driver: null,
69
+ consoleBuffer: new ConsoleBuffer(config.bufferSize),
70
+ elementRefMap: new ElementRefMap(),
71
+ networkBuffer: new NetworkBuffer(),
72
+ dialogQueue: new DialogQueue(),
73
+ downloadTracker: new DownloadTracker(),
74
+ traceState: createTraceState(),
69
75
  baseConfig: config
70
76
  };
71
77
 
72
- function attachEventHandlers(manager: BrowserManager): void {
73
- let monitoredClient: unknown = null;
74
-
75
- function ensureMonitors(): void {
76
- const client = manager.getClient();
77
- if (client && client !== monitoredClient) {
78
- attachNetworkMonitor(client, networkBuffer);
79
- attachDialogHandler(client, dialogQueue);
80
- attachDownloadHandler(client, downloadTracker);
81
- void enableDownloads(client).catch(() => {
82
- // Download behavior setup may fail in some environments
83
- });
84
- monitoredClient = client;
85
- }
86
- }
87
-
88
- manager.setEventCallback((event, data) => {
89
- if (event === 'console_message') {
90
- consoleBuffer.push(data as ConsoleMessage);
91
- }
92
- if (event === 'page_navigated') {
93
- elementRefMap.invalidate();
94
- }
95
- if (event === 'browser_connected') {
96
- ensureMonitors();
97
- }
98
- if (event === 'browser_disconnected') {
99
- monitoredClient = null;
100
- resetCssState();
101
- dialogQueue.clear();
102
- downloadTracker.clear();
103
- }
104
- // Reattach after tab switch (new client, no connect event)
105
- ensureMonitors();
106
- });
107
- }
108
-
109
- function makeBrowserManager(): BrowserManager {
110
- const browserConfig = loadConfig({
111
- cdpPort: config.cdpPort,
112
- cdpHost: config.cdpHost
113
- });
114
- const manager = new BrowserManager(browserConfig);
115
- attachEventHandlers(manager);
116
- return manager;
117
- }
78
+ const makeDriver: DriverFactory = () =>
79
+ createDriver(
80
+ 'chrome',
81
+ loadConfig({ cdpPort: config.cdpPort, cdpHost: config.cdpHost }),
82
+ context
83
+ );
118
84
 
119
- registerBrowserTools(server, context, makeBrowserManager);
120
- registerErrorTools(server, consoleBuffer);
121
- registerSnapshotTools(server, context);
122
- registerInteractionTools(server, context);
123
- registerNetworkTools(server, context);
124
- registerRuntimeTools(server, consoleBuffer);
125
- registerStylesTools(server, context);
126
- registerFilesTools(server, context);
127
- registerPerformanceTools(server, context);
128
- registerConnectTools(server, context, makeBrowserManager);
85
+ registerAllTools(server, context, makeDriver);
129
86
 
130
87
  return {
131
88
  server,
132
89
  cleanup: async () => {
133
- if (context.manager) {
134
- await context.manager.destroy();
135
- context.manager = null;
90
+ if (context.driver) {
91
+ await context.driver.destroy();
92
+ context.driver = null;
136
93
  }
137
94
  }
138
95
  };
@@ -1,32 +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';
3
+ import type { BrowserContext, DriverFactory } from '../server.js';
6
4
  import { DEFAULT_STARTUP_TIMEOUT_MS } from '../../browser/chrome-launcher.js';
7
5
  import {
8
6
  listProfiles,
9
7
  pilotDataDir,
10
8
  stopPilotBrowsers
11
9
  } from '../../browser/sessions.js';
12
- import { executeScreenshot } from '../../commands/screenshot.js';
13
- import { executeNavigate } from '../../commands/navigate.js';
14
- import { executeClick } from '../../commands/click.js';
15
- import { executeType } from '../../commands/type.js';
16
- import { executeEvaluate } from '../../commands/evaluate.js';
17
- import { executeWait } from '../../commands/wait.js';
18
- import { executePageInfo } from '../../commands/page_info.js';
19
- import {
20
- executeListTabs,
21
- executeNewTab,
22
- executeCloseTab,
23
- executeSwitchTab
24
- } from '../../commands/tabs.js';
25
- import {
26
- VIEWPORT_PRESETS,
27
- applyViewport,
28
- type ViewportConfig
29
- } from '../../viewport.js';
10
+ import { VIEWPORT_PRESETS } from '../../viewport.js';
11
+ import { requireDriver } from './driver-context.js';
30
12
 
31
13
  // Raw shapes for MCP SDK (expects ZodRawShape, not ZodObject)
32
14
 
@@ -248,22 +230,10 @@ const viewportResizeShape = {
248
230
  )
249
231
  };
250
232
 
251
- function requireClient(context: BrowserContext): {
252
- client: Client;
253
- manager: BrowserManager;
254
- } {
255
- const manager = context.manager;
256
- if (!manager || !manager.isConnected()) {
257
- throw new Error('Browser not started. Call browser_start first.');
258
- }
259
- const client = manager.getClient()!;
260
- return { client, manager };
261
- }
262
-
263
233
  export function registerBrowserTools(
264
234
  server: McpServer,
265
235
  context: BrowserContext,
266
- makeBrowserManager: () => BrowserManager
236
+ makeDriver: DriverFactory
267
237
  ): void {
268
238
  server.tool(
269
239
  'browser_start',
@@ -280,7 +250,7 @@ export function registerBrowserTools(
280
250
  responsive,
281
251
  startupTimeoutMs
282
252
  }) => {
283
- if (context.manager?.isConnected()) {
253
+ if (context.driver?.isConnected()) {
284
254
  return {
285
255
  content: [
286
256
  {
@@ -291,14 +261,14 @@ export function registerBrowserTools(
291
261
  };
292
262
  }
293
263
 
294
- if (context.manager) {
295
- await context.manager.destroy();
296
- context.manager = null;
264
+ if (context.driver) {
265
+ await context.driver.destroy();
266
+ context.driver = null;
297
267
  }
298
268
 
299
- const manager = makeBrowserManager();
269
+ const driver = makeDriver();
300
270
  const { headless: effectiveHeadless, viewport: effectiveViewport } =
301
- await manager.launch({
271
+ await driver.launch({
302
272
  headless,
303
273
  chromePath,
304
274
  profileName,
@@ -309,7 +279,7 @@ export function registerBrowserTools(
309
279
  responsive,
310
280
  startupTimeoutMs
311
281
  });
312
- context.manager = manager;
282
+ context.driver = driver;
313
283
 
314
284
  return {
315
285
  content: [
@@ -327,7 +297,7 @@ export function registerBrowserTools(
327
297
  'Stop the Chrome browser and free all resources.',
328
298
  {},
329
299
  async () => {
330
- if (!context.manager) {
300
+ if (!context.driver) {
331
301
  return {
332
302
  content: [
333
303
  {
@@ -338,8 +308,8 @@ export function registerBrowserTools(
338
308
  };
339
309
  }
340
310
 
341
- await context.manager.destroy();
342
- context.manager = null;
311
+ await context.driver.destroy();
312
+ context.driver = null;
343
313
 
344
314
  return {
345
315
  content: [{ type: 'text' as const, text: 'Browser stopped.' }]
@@ -353,7 +323,7 @@ export function registerBrowserTools(
353
323
  {},
354
324
  async () => {
355
325
  const status = {
356
- session: context.manager?.getSessionInfo() ?? {
326
+ session: context.driver?.getSessionInfo() ?? {
357
327
  mode: 'none',
358
328
  connected: false
359
329
  },
@@ -374,10 +344,10 @@ export function registerBrowserTools(
374
344
  {},
375
345
  async () => {
376
346
  const lines: string[] = [];
377
- if (context.manager) {
378
- const info = context.manager.getSessionInfo();
379
- await context.manager.destroy();
380
- context.manager = null;
347
+ if (context.driver) {
348
+ const info = context.driver.getSessionInfo();
349
+ await context.driver.destroy();
350
+ context.driver = null;
381
351
  if (info.mode === 'launched') {
382
352
  lines.push(
383
353
  `this session: stopped Chrome pid ${info.pid} (profile "${info.profile}")`
@@ -410,8 +380,7 @@ export function registerBrowserTools(
410
380
  'Capture a screenshot of the current browser viewport or full page',
411
381
  screenshotShape,
412
382
  async ({ format, quality, fullPage }) => {
413
- const { client } = requireClient(context);
414
- const result = await executeScreenshot(client, {
383
+ const result = await requireDriver(context).screenshot({
415
384
  format,
416
385
  quality,
417
386
  fullPage
@@ -446,8 +415,7 @@ export function registerBrowserTools(
446
415
  'Navigate to a URL and wait for page load',
447
416
  navigateShape,
448
417
  async ({ url, waitUntil, timeoutMs }) => {
449
- const { client, manager } = requireClient(context);
450
- const result = await executeNavigate(client, manager, {
418
+ const result = await requireDriver(context).navigate({
451
419
  url,
452
420
  waitUntil,
453
421
  timeoutMs
@@ -485,8 +453,7 @@ export function registerBrowserTools(
485
453
  };
486
454
  }
487
455
 
488
- const { client } = requireClient(context);
489
- const result = await executeClick(client, params, context.elementRefMap);
456
+ const result = await requireDriver(context).click(params);
490
457
 
491
458
  return {
492
459
  content: [
@@ -504,8 +471,7 @@ export function registerBrowserTools(
504
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).',
505
472
  typeShape,
506
473
  async (params) => {
507
- const { client } = requireClient(context);
508
- await executeType(client, params, context.elementRefMap);
474
+ await requireDriver(context).type(params);
509
475
 
510
476
  const target = params.ref ?? params.selector;
511
477
  return {
@@ -524,8 +490,7 @@ export function registerBrowserTools(
524
490
  'Execute JavaScript in the page context and return the result',
525
491
  evaluateShape,
526
492
  async ({ expression, awaitPromise, timeoutMs }) => {
527
- const { client } = requireClient(context);
528
- const result = await executeEvaluate(client, {
493
+ const result = await requireDriver(context).evaluate({
529
494
  expression,
530
495
  awaitPromise,
531
496
  timeoutMs
@@ -568,8 +533,7 @@ export function registerBrowserTools(
568
533
  };
569
534
  }
570
535
 
571
- const { client, manager } = requireClient(context);
572
- const result = await executeWait(client, manager, params);
536
+ const result = await requireDriver(context).wait(params);
573
537
 
574
538
  return {
575
539
  content: [
@@ -587,8 +551,7 @@ export function registerBrowserTools(
587
551
  'Get information about the current page (URL, title, ready state)',
588
552
  {},
589
553
  async () => {
590
- const { client } = requireClient(context);
591
- const result = await executePageInfo(client);
554
+ const result = await requireDriver(context).pageInfo();
592
555
 
593
556
  return {
594
557
  content: [
@@ -606,8 +569,7 @@ export function registerBrowserTools(
606
569
  'List all open browser tabs with their target IDs, URLs, titles, and active status',
607
570
  {},
608
571
  async () => {
609
- const { manager } = requireClient(context);
610
- const tabs = await executeListTabs(manager);
572
+ const tabs = await requireDriver(context).listTabs();
611
573
 
612
574
  return {
613
575
  content: [
@@ -625,8 +587,7 @@ export function registerBrowserTools(
625
587
  'Open a new browser tab, optionally navigating to a URL',
626
588
  newTabShape,
627
589
  async ({ url }) => {
628
- const { manager } = requireClient(context);
629
- const result = await executeNewTab(manager, url);
590
+ const result = await requireDriver(context).newTab(url);
630
591
 
631
592
  return {
632
593
  content: [
@@ -644,8 +605,7 @@ export function registerBrowserTools(
644
605
  'Close a browser tab by its target ID. Cannot close the currently active tab.',
645
606
  closeTabShape,
646
607
  async ({ targetId }) => {
647
- const { manager } = requireClient(context);
648
- await executeCloseTab(manager, targetId);
608
+ await requireDriver(context).closeTab(targetId);
649
609
 
650
610
  return {
651
611
  content: [
@@ -663,8 +623,7 @@ export function registerBrowserTools(
663
623
  'Switch the active browser tab to the one with the given target ID. All subsequent commands will target this tab.',
664
624
  switchTabShape,
665
625
  async ({ targetId }) => {
666
- const { manager } = requireClient(context);
667
- await executeSwitchTab(manager, targetId);
626
+ await requireDriver(context).switchTab(targetId);
668
627
 
669
628
  return {
670
629
  content: [
@@ -682,22 +641,12 @@ export function registerBrowserTools(
682
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.',
683
642
  viewportResizeShape,
684
643
  async ({ width, height, deviceScaleFactor, mobile }) => {
685
- const { client, manager } = requireClient(context);
686
-
687
- const current = manager.getViewportConfig();
688
- const effectiveMobile = mobile ?? current?.mobile ?? false;
689
- const newConfig: ViewportConfig = {
644
+ const newConfig = await requireDriver(context).resizeViewport({
690
645
  width,
691
646
  height,
692
- deviceScaleFactor: deviceScaleFactor ?? current?.deviceScaleFactor ?? 1,
693
- mobile: effectiveMobile,
694
- responsive: false,
695
- ...(effectiveMobile &&
696
- current?.userAgent && { userAgent: current.userAgent })
697
- };
698
-
699
- await applyViewport(client, newConfig);
700
- manager.setViewportConfig(newConfig);
647
+ deviceScaleFactor,
648
+ mobile
649
+ });
701
650
 
702
651
  return {
703
652
  content: [
@@ -715,11 +664,11 @@ export function registerBrowserTools(
715
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).',
716
665
  tabsShape,
717
666
  async ({ action, targetId, url }) => {
718
- const { manager } = requireClient(context);
667
+ const driver = requireDriver(context);
719
668
 
720
669
  switch (action) {
721
670
  case 'list': {
722
- const tabs = await executeListTabs(manager);
671
+ const tabs = await driver.listTabs();
723
672
  return {
724
673
  content: [
725
674
  { type: 'text' as const, text: JSON.stringify(tabs, null, 2) }
@@ -728,7 +677,7 @@ export function registerBrowserTools(
728
677
  }
729
678
 
730
679
  case 'new': {
731
- const result = await executeNewTab(manager, url);
680
+ const result = await driver.newTab(url);
732
681
  return {
733
682
  content: [
734
683
  {
@@ -751,7 +700,7 @@ export function registerBrowserTools(
751
700
  isError: true
752
701
  };
753
702
  }
754
- await executeSwitchTab(manager, targetId);
703
+ await driver.switchTab(targetId);
755
704
  return {
756
705
  content: [
757
706
  {
@@ -774,7 +723,7 @@ export function registerBrowserTools(
774
723
  isError: true
775
724
  };
776
725
  }
777
- await executeCloseTab(manager, targetId);
726
+ await driver.closeTab(targetId);
778
727
  return {
779
728
  content: [
780
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,