@narumitw/pi-chrome-devtools 0.52.2 → 0.53.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 (35) hide show
  1. package/README.md +78 -17
  2. package/dist/chunks/{browser-settings-menu-VI2I3TXI.js → browser-settings-menu-N5O65I7S.js} +81 -7
  3. package/dist/chunks/browser-settings-menu-N5O65I7S.js.map +7 -0
  4. package/dist/chunks/chunk-B7B63PWB.js +465 -0
  5. package/dist/chunks/chunk-B7B63PWB.js.map +7 -0
  6. package/dist/chunks/{chunk-CC37WUUE.js → chunk-GVBXT3FB.js} +369 -461
  7. package/dist/chunks/chunk-GVBXT3FB.js.map +7 -0
  8. package/dist/chunks/chunk-RTECHAEZ.js +373 -0
  9. package/dist/chunks/chunk-RTECHAEZ.js.map +7 -0
  10. package/dist/chunks/{menu-LLR455UX.js → menu-UBHW6UP2.js} +75 -41
  11. package/dist/chunks/menu-UBHW6UP2.js.map +7 -0
  12. package/dist/chunks/tools-V7OR5KJ3.js +945 -0
  13. package/dist/chunks/tools-V7OR5KJ3.js.map +7 -0
  14. package/dist/index.ts +166 -196
  15. package/dist/index.ts.map +4 -4
  16. package/package.json +4 -3
  17. package/src/browser-manager.ts +282 -109
  18. package/src/browser-settings-menu.ts +98 -4
  19. package/src/cdp-client.ts +329 -83
  20. package/src/chrome-devtools.ts +61 -13
  21. package/src/lazy-tools.ts +105 -8
  22. package/src/menu.ts +83 -41
  23. package/src/render.ts +12 -1
  24. package/src/runtime.ts +92 -0
  25. package/src/settings.ts +47 -10
  26. package/src/tool-names.ts +16 -1
  27. package/src/tool-selector.ts +79 -36
  28. package/src/tools.ts +86 -10
  29. package/src/webmcp/discovery.ts +235 -0
  30. package/src/webmcp/policy.ts +402 -0
  31. package/src/webmcp/protocol.ts +414 -0
  32. package/src/webmcp/tools.ts +190 -0
  33. package/dist/chunks/browser-settings-menu-VI2I3TXI.js.map +0 -7
  34. package/dist/chunks/chunk-CC37WUUE.js.map +0 -7
  35. package/dist/chunks/menu-LLR455UX.js.map +0 -7
@@ -8,16 +8,20 @@ import {
8
8
  BROWSER_SHUTDOWN_WAIT_MS,
9
9
  type BrowserCandidate,
10
10
  type BrowserCandidateDefinition,
11
+ type BrowserLaunchAttempt,
11
12
  DEFAULT_ENDPOINT_RETRY_MS,
12
13
  DEFAULT_ENDPOINT_WAIT_MS,
13
14
  DEFAULT_HTTP_TIMEOUT_MS,
14
15
  DEVTOOLS_ACTIVE_PORT_FILE,
15
16
  type DevToolsPage,
17
+ invalidateWebMcpOperations,
16
18
  MANAGED_BROWSER_PROFILE_PREFIX,
17
19
  type ManagedBrowser,
18
20
  state,
19
21
  } from "./runtime.js";
20
22
 
23
+ // Endpoint attachment, managed launch, dynamic-port publication, and shutdown intentionally remain
24
+ // together because they form one ownership state machine behind the testable operations boundary.
21
25
  interface BrowserSpawnOptions {
22
26
  shell: false;
23
27
  stdio: "ignore";
@@ -49,6 +53,80 @@ const DEFAULT_BROWSER_MANAGER_OPERATIONS: BrowserManagerOperations = {
49
53
 
50
54
  let browserManagerOperations = DEFAULT_BROWSER_MANAGER_OPERATIONS;
51
55
 
56
+ interface ManagedBrowserScope {
57
+ controller: AbortController;
58
+ generation: number;
59
+ launchPromise?: Promise<void>;
60
+ managedBrowser?: ManagedBrowser;
61
+ port: number;
62
+ lastLaunchAttempt?: BrowserLaunchAttempt;
63
+ }
64
+
65
+ const managedBrowserScopes = new WeakMap<object, ManagedBrowserScope>();
66
+
67
+ function managedBrowserScope(owner: object) {
68
+ const existing = managedBrowserScopes.get(owner);
69
+ if (existing) return existing;
70
+ const created: ManagedBrowserScope = {
71
+ controller: new AbortController(),
72
+ generation: 0,
73
+ port: state.configuredPort,
74
+ };
75
+ managedBrowserScopes.set(owner, created);
76
+ return created;
77
+ }
78
+
79
+ export function startManagedBrowserSession(owner: object) {
80
+ const scope = managedBrowserScope(owner);
81
+ scope.controller.abort(new DOMException("Chrome DevTools session replaced", "AbortError"));
82
+ scope.controller = new AbortController();
83
+ scope.generation += 1;
84
+ scope.port = state.configuredPort;
85
+ scope.lastLaunchAttempt = undefined;
86
+ }
87
+
88
+ export function managedBrowserForOwner(owner?: object) {
89
+ if (!owner) return state.managedBrowser;
90
+ const scopedBrowser = managedBrowserScope(owner).managedBrowser;
91
+ if (scopedBrowser) return scopedBrowser;
92
+ return state.managedBrowser?.sessionOwner === owner ? state.managedBrowser : undefined;
93
+ }
94
+
95
+ function setManagedBrowser(owner: object | undefined, browser: ManagedBrowser | undefined) {
96
+ if (owner) managedBrowserScope(owner).managedBrowser = browser;
97
+ else state.managedBrowser = browser;
98
+ }
99
+
100
+ function launchPromiseForOwner(owner?: object) {
101
+ return owner ? managedBrowserScope(owner).launchPromise : state.launchPromise;
102
+ }
103
+
104
+ function setLaunchPromise(owner: object | undefined, launchPromise: Promise<void> | undefined) {
105
+ if (owner) managedBrowserScope(owner).launchPromise = launchPromise;
106
+ else state.launchPromise = launchPromise;
107
+ }
108
+
109
+ function managedBrowserPort(owner?: object) {
110
+ return owner ? managedBrowserScope(owner).port : state.port;
111
+ }
112
+
113
+ function setManagedBrowserPort(owner: object | undefined, port: number) {
114
+ if (owner) managedBrowserScope(owner).port = port;
115
+ else state.port = port;
116
+ }
117
+
118
+ function lastLaunchAttempt(owner?: object) {
119
+ return owner ? managedBrowserScope(owner).lastLaunchAttempt : state.lastLaunchAttempt;
120
+ }
121
+
122
+ function setLastLaunchAttempt(
123
+ owner: object | undefined,
124
+ attempt: BrowserLaunchAttempt | undefined,
125
+ ) {
126
+ if (owner) managedBrowserScope(owner).lastLaunchAttempt = attempt;
127
+ else state.lastLaunchAttempt = attempt;
128
+ }
129
+
52
130
  export function setBrowserManagerOperationsForTests(overrides: Partial<BrowserManagerOperations>) {
53
131
  const previous = browserManagerOperations;
54
132
  browserManagerOperations = { ...DEFAULT_BROWSER_MANAGER_OPERATIONS, ...overrides };
@@ -69,19 +147,28 @@ function normalizePathForComparison(value: string) {
69
147
  return process.platform === "win32" ? value.toLowerCase() : value;
70
148
  }
71
149
 
72
- export async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS) {
150
+ export async function ensureDevToolsEndpoint(
151
+ waitMs = DEFAULT_ENDPOINT_WAIT_MS,
152
+ callerSignal?: AbortSignal,
153
+ owner?: object,
154
+ ) {
155
+ callerSignal?.throwIfAborted();
73
156
  if (extensionsConfigured()) {
74
157
  validateExtensionLaunchMode();
75
- await ensureManagedBrowserLaunched(waitMs);
158
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
76
159
  return;
77
160
  }
78
161
  if (canAutoLaunchBrowser()) {
79
162
  try {
80
- await withEndpointRetry(() => fetchDevToolsJson<unknown>("/json/version"), waitMs);
163
+ await withEndpointRetry(
164
+ () => fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner),
165
+ waitMs,
166
+ callerSignal,
167
+ );
81
168
  return;
82
169
  } catch (error) {
83
170
  if (shouldAutoLaunchAfterEndpointError(error)) {
84
- await ensureManagedBrowserLaunched(waitMs);
171
+ await ensureManagedBrowserLaunched(waitMs, callerSignal, owner);
85
172
  return;
86
173
  }
87
174
  throw error;
@@ -89,7 +176,7 @@ export async function ensureDevToolsEndpoint(waitMs = DEFAULT_ENDPOINT_WAIT_MS)
89
176
  }
90
177
 
91
178
  try {
92
- await fetchDevToolsJson<unknown>("/json/version");
179
+ await fetchDevToolsJson<unknown>("/json/version", { signal: callerSignal }, owner);
93
180
  } catch (error) {
94
181
  if (isRetryableEndpointError(error)) return;
95
182
  throw error;
@@ -114,51 +201,97 @@ function validateExtensionLaunchMode() {
114
201
  }
115
202
  }
116
203
 
117
- async function ensureManagedBrowserLaunched(waitMs: number) {
118
- if (state.launchPromise) return state.launchPromise;
119
- if (state.managedBrowser && !state.managedBrowser.exited && state.managedBrowser.ready) return;
120
- const generation = state.sessionGeneration;
121
- const signal = state.sessionController.signal;
122
- throwIfBrowserLaunchCancelled(generation, signal);
123
-
124
- const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal);
204
+ async function ensureManagedBrowserLaunched(
205
+ waitMs: number,
206
+ callerSignal?: AbortSignal,
207
+ owner?: object,
208
+ ) {
209
+ const existingLaunch = launchPromiseForOwner(owner);
210
+ if (existingLaunch) {
211
+ await waitForCaller(existingLaunch, callerSignal);
212
+ return;
213
+ }
214
+ const existingBrowser = managedBrowserForOwner(owner);
215
+ if (existingBrowser && !existingBrowser.exited && existingBrowser.ready) return;
216
+ const generation = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
217
+ const signal = owner
218
+ ? managedBrowserScope(owner).controller.signal
219
+ : state.sessionController.signal;
220
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
221
+
222
+ const launchPromise = prepareManagedBrowserLaunch(waitMs, generation, signal, owner);
125
223
  const wrappedPromise = launchPromise.finally(() => {
126
- if (state.launchPromise === wrappedPromise) state.launchPromise = undefined;
224
+ if (launchPromiseForOwner(owner) === wrappedPromise) setLaunchPromise(owner, undefined);
225
+ });
226
+ setLaunchPromise(owner, wrappedPromise);
227
+ await waitForCaller(wrappedPromise, callerSignal);
228
+ }
229
+
230
+ function waitForCaller<T>(operation: Promise<T>, signal?: AbortSignal) {
231
+ if (!signal) return operation;
232
+ signal.throwIfAborted();
233
+ return new Promise<T>((resolve, reject) => {
234
+ let settled = false;
235
+ const onAbort = () => settle(() => reject(signal.reason));
236
+ const settle = (callback: () => void) => {
237
+ if (settled) return;
238
+ settled = true;
239
+ signal.removeEventListener("abort", onAbort);
240
+ callback();
241
+ };
242
+ signal.addEventListener("abort", onAbort, { once: true });
243
+ operation.then(
244
+ (value) => settle(() => resolve(value)),
245
+ (error) => settle(() => reject(error)),
246
+ );
127
247
  });
128
- state.launchPromise = wrappedPromise;
129
- return wrappedPromise;
130
248
  }
131
249
 
132
250
  async function prepareManagedBrowserLaunch(
133
251
  waitMs: number,
134
252
  generation: number,
135
253
  signal: AbortSignal,
254
+ owner?: object,
136
255
  ) {
137
- if (state.managedBrowser) {
138
- await shutdownManagedBrowser(state.managedBrowser, { awaitLaunch: false });
139
- throwIfBrowserLaunchCancelled(generation, signal);
256
+ const existingBrowser = managedBrowserForOwner(owner);
257
+ if (existingBrowser) {
258
+ await shutdownManagedBrowser(existingBrowser, { awaitLaunch: false, owner });
259
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
140
260
  }
141
261
  if (state.portConfigured) {
142
- const available = await browserManagerOperations.isPortAvailable(state.host, state.port);
143
- throwIfBrowserLaunchCancelled(generation, signal);
262
+ const available = await browserManagerOperations.isPortAvailable(
263
+ state.host,
264
+ managedBrowserPort(owner),
265
+ );
266
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
144
267
  if (!available) {
145
268
  throw new DevToolsEndpointError(
146
- `Cannot launch the extension-owned browser because explicit port ${state.port} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
269
+ `Cannot launch the extension-owned browser because explicit port ${managedBrowserPort(owner)} is already in use. Choose a free browser.endpoint port or reset the endpoint to use a dynamic managed port.`,
147
270
  );
148
271
  }
149
272
  }
150
- return launchManagedBrowser(waitMs, generation, signal);
273
+ return launchManagedBrowser(waitMs, generation, signal, owner);
151
274
  }
152
275
 
153
- async function launchManagedBrowser(waitMs: number, generation: number, signal: AbortSignal) {
154
- throwIfBrowserLaunchCancelled(generation, signal);
276
+ async function launchManagedBrowser(
277
+ waitMs: number,
278
+ generation: number,
279
+ signal: AbortSignal,
280
+ owner?: object,
281
+ ) {
282
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
155
283
  const candidateDefinitions = browserCandidateDefinitions();
156
- const candidates = await resolveBrowserCandidates(candidateDefinitions, generation, signal);
157
- throwIfBrowserLaunchCancelled(generation, signal);
158
- state.lastLaunchAttempt = {
284
+ const candidates = await resolveBrowserCandidates(
285
+ candidateDefinitions,
286
+ generation,
287
+ signal,
288
+ owner,
289
+ );
290
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
291
+ setLastLaunchAttempt(owner, {
159
292
  candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
160
293
  mode: state.portConfigured ? "explicit-port" : "dynamic-port",
161
- };
294
+ });
162
295
 
163
296
  if (candidates.length === 0) {
164
297
  throw new DevToolsEndpointError(noBrowserCandidateMessage(candidateDefinitions));
@@ -166,33 +299,41 @@ async function launchManagedBrowser(waitMs: number, generation: number, signal:
166
299
 
167
300
  let lastError: unknown;
168
301
  for (const candidate of candidates) {
169
- throwIfBrowserLaunchCancelled(generation, signal);
302
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
170
303
  try {
171
304
  if (extensionsConfigured())
172
- await requireSupportedExtensionBrowser(candidate, generation, signal);
173
- await launchBrowserCandidate(candidate, waitMs, generation, signal);
174
- throwIfBrowserLaunchCancelled(generation, signal);
175
- state.lastLaunchAttempt = {
176
- ...state.lastLaunchAttempt,
305
+ await requireSupportedExtensionBrowser(candidate, generation, signal, owner);
306
+ await launchBrowserCandidate(candidate, waitMs, generation, signal, owner);
307
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
308
+ setLastLaunchAttempt(owner, {
309
+ ...lastLaunchAttempt(owner),
310
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
311
+ mode: state.portConfigured ? "explicit-port" : "dynamic-port",
177
312
  selectedCandidate: formatBrowserCandidate(candidate),
178
- userDataDir: state.managedBrowser?.userDataDir,
179
- };
313
+ userDataDir: managedBrowserForOwner(owner)?.userDataDir,
314
+ });
180
315
  return;
181
316
  } catch (error) {
182
317
  try {
183
- throwIfBrowserLaunchCancelled(generation, signal);
318
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
184
319
  } catch (cancellation) {
185
- state.lastLaunchAttempt = {
186
- ...state.lastLaunchAttempt,
320
+ setLastLaunchAttempt(owner, {
321
+ ...(lastLaunchAttempt(owner) ?? {
322
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
323
+ mode: state.portConfigured ? "explicit-port" : "dynamic-port",
324
+ }),
187
325
  lastError: formatError(cancellation),
188
- };
326
+ });
189
327
  throw cancellation;
190
328
  }
191
329
  lastError = error;
192
- state.lastLaunchAttempt = {
193
- ...state.lastLaunchAttempt,
330
+ setLastLaunchAttempt(owner, {
331
+ ...(lastLaunchAttempt(owner) ?? {
332
+ candidateLabels: candidateDefinitions.map(formatBrowserCandidateDefinition),
333
+ mode: state.portConfigured ? "explicit-port" : "dynamic-port",
334
+ }),
194
335
  lastError: formatError(error),
195
- };
336
+ });
196
337
  }
197
338
  }
198
339
 
@@ -214,6 +355,7 @@ async function requireSupportedExtensionBrowser(
214
355
  candidate: BrowserCandidate,
215
356
  generation: number,
216
357
  signal: AbortSignal,
358
+ owner?: object,
217
359
  ) {
218
360
  let version: string;
219
361
  try {
@@ -221,12 +363,12 @@ async function requireSupportedExtensionBrowser(
221
363
  await browserManagerOperations.inspectBrowserVersion(candidate.resolvedExecutable, signal)
222
364
  ).trim();
223
365
  } catch (error) {
224
- throwIfBrowserLaunchCancelled(generation, signal);
366
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
225
367
  throw new DevToolsEndpointError(
226
368
  `Unable to identify the configured extension browser ${candidate.resolvedExecutable}: ${formatError(error)}. Configure Chrome for Testing or Chromium.`,
227
369
  );
228
370
  }
229
- throwIfBrowserLaunchCancelled(generation, signal);
371
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
230
372
  const classification = classifyExtensionBrowserVersion(version);
231
373
  if (!classification.supported) {
232
374
  throw new DevToolsEndpointError(
@@ -252,15 +394,16 @@ async function launchBrowserCandidate(
252
394
  waitMs: number,
253
395
  generation: number,
254
396
  signal: AbortSignal,
397
+ owner?: object,
255
398
  ) {
256
- throwIfBrowserLaunchCancelled(generation, signal);
399
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
257
400
  const userDataDir = await browserManagerOperations.mkdtemp(
258
401
  join(tmpdir(), MANAGED_BROWSER_PROFILE_PREFIX),
259
402
  );
260
403
  let managedBrowser: ManagedBrowser | undefined;
261
404
  try {
262
- throwIfBrowserLaunchCancelled(generation, signal);
263
- const portArgument = state.portConfigured ? String(state.port) : "0";
405
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
406
+ const portArgument = state.portConfigured ? String(managedBrowserPort(owner)) : "0";
264
407
  const args = buildManagedBrowserLaunchArguments(
265
408
  userDataDir,
266
409
  portArgument,
@@ -276,22 +419,26 @@ async function launchBrowserCandidate(
276
419
  exited: false,
277
420
  ready: false,
278
421
  ownerGeneration: generation,
422
+ ...(owner ? { sessionOwner: owner } : {}),
279
423
  };
280
424
  managedBrowser = launchedBrowser;
281
- state.managedBrowser = launchedBrowser;
425
+ setManagedBrowser(owner, launchedBrowser);
282
426
 
283
427
  child.once("exit", () => {
428
+ if (managedBrowserForOwner(owner) === launchedBrowser && owner) {
429
+ invalidateWebMcpOperations(owner, "Chrome DevTools managed browser exited");
430
+ }
284
431
  launchedBrowser.exited = true;
285
432
  launchedBrowser.ready = false;
286
- if (!state.portConfigured && launchedBrowser.port === state.port) {
287
- state.port = state.configuredPort;
433
+ if (!state.portConfigured && launchedBrowser.port === managedBrowserPort(owner)) {
434
+ setManagedBrowserPort(owner, state.configuredPort);
288
435
  }
289
436
  });
290
437
 
291
438
  await waitForBrowserSpawn(child);
292
- throwIfBrowserLaunchCancelled(generation, signal);
439
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
293
440
  if (state.portConfigured) {
294
- launchedBrowser.port = state.port;
441
+ launchedBrowser.port = managedBrowserPort(owner);
295
442
  } else {
296
443
  launchedBrowser.port = await readManagedBrowserPort(
297
444
  userDataDir,
@@ -299,15 +446,16 @@ async function launchBrowserCandidate(
299
446
  waitMs,
300
447
  generation,
301
448
  signal,
449
+ owner,
302
450
  );
303
- throwIfBrowserLaunchCancelled(generation, signal);
304
- state.port = launchedBrowser.port;
451
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
452
+ setManagedBrowserPort(owner, launchedBrowser.port);
305
453
  }
306
- await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal);
307
- throwIfBrowserLaunchCancelled(generation, signal);
454
+ await waitForDevToolsEndpoint(waitMs, launchedBrowser, generation, signal, owner);
455
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
308
456
  launchedBrowser.ready = true;
309
457
  } catch (error) {
310
- if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false });
458
+ if (managedBrowser) await shutdownManagedBrowser(managedBrowser, { awaitLaunch: false, owner });
311
459
  else {
312
460
  await browserManagerOperations
313
461
  .rm(userDataDir, { recursive: true, force: true })
@@ -362,6 +510,7 @@ async function readManagedBrowserPort(
362
510
  waitMs: number,
363
511
  generation: number,
364
512
  signal: AbortSignal,
513
+ owner?: object,
365
514
  ) {
366
515
  const activePortFile = join(userDataDir, DEVTOOLS_ACTIVE_PORT_FILE);
367
516
  const deadline = Date.now() + waitMs;
@@ -373,7 +522,7 @@ async function readManagedBrowserPort(
373
522
  if (isNodeError(error) && error.code === "ENOENT") return undefined;
374
523
  throw error;
375
524
  });
376
- throwIfBrowserLaunchCancelled(generation, signal);
525
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
377
526
  const portText = text?.split(/\r?\n/, 1)[0]?.trim();
378
527
  const port = Number(portText);
379
528
  if (Number.isInteger(port) && port > 0) return port;
@@ -397,24 +546,25 @@ async function waitForDevToolsEndpoint(
397
546
  managedBrowser: ManagedBrowser,
398
547
  generation: number,
399
548
  signal: AbortSignal,
549
+ owner?: object,
400
550
  ) {
401
551
  const deadline = Date.now() + waitMs;
402
552
  while (true) {
403
553
  throwIfManagedBrowserExited(managedBrowser);
404
554
  try {
405
- await fetchDevToolsJson<unknown>("/json/version", { signal });
406
- throwIfBrowserLaunchCancelled(generation, signal);
555
+ await fetchDevToolsJson<unknown>("/json/version", { signal }, owner);
556
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
407
557
  return;
408
558
  } catch (error) {
409
559
  if (!isRetryableEndpointError(error)) throw error;
410
560
  }
411
561
 
412
- throwIfBrowserLaunchCancelled(generation, signal);
562
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
413
563
  const remainingMs = deadline - Date.now();
414
564
  if (remainingMs <= 0) {
415
565
  throw new DevToolsEndpointError(
416
566
  [
417
- `Timed out waiting for auto-launched browser at ${devToolsEndpoint()}.`,
567
+ `Timed out waiting for auto-launched browser at ${devToolsEndpoint(owner)}.`,
418
568
  launchHint(),
419
569
  ].join("\n"),
420
570
  );
@@ -428,36 +578,52 @@ function throwIfManagedBrowserExited(managedBrowser: ManagedBrowser) {
428
578
  throw new DevToolsEndpointError("Auto-launched browser exited before DevTools became available.");
429
579
  }
430
580
 
431
- function throwIfBrowserLaunchCancelled(generation: number, signal: AbortSignal) {
432
- if (!state.shuttingDown && generation === state.sessionGeneration && !signal.aborted) {
433
- return;
434
- }
581
+ function throwIfBrowserLaunchCancelled(generation: number, signal: AbortSignal, owner?: object) {
582
+ const currentGeneration = owner ? managedBrowserScope(owner).generation : state.sessionGeneration;
583
+ const shuttingDown = owner ? signal.aborted : state.shuttingDown;
584
+ if (!shuttingDown && generation === currentGeneration && !signal.aborted) return;
435
585
  throw new DevToolsEndpointError(
436
- generation === state.sessionGeneration
586
+ generation === currentGeneration
437
587
  ? "Chrome DevTools browser launch cancelled during shutdown."
438
588
  : "Chrome DevTools browser launch cancelled because the session was replaced.",
439
589
  );
440
590
  }
441
591
 
442
592
  export async function shutdownManagedBrowser(
443
- managedBrowser = state.managedBrowser,
444
- options: { awaitLaunch?: boolean; cancelLaunch?: boolean } = {},
593
+ managedBrowser?: ManagedBrowser,
594
+ options: { awaitLaunch?: boolean; cancelLaunch?: boolean; owner?: object } = {},
445
595
  ) {
446
- if (options.cancelLaunch) state.shuttingDown = true;
596
+ const owner = options.owner ?? managedBrowser?.sessionOwner;
597
+ if (options.cancelLaunch) {
598
+ if (owner) {
599
+ const scope = managedBrowserScope(owner);
600
+ scope.controller.abort(
601
+ new DOMException("Chrome DevTools browser launch cancelled during shutdown", "AbortError"),
602
+ );
603
+ scope.generation += 1;
604
+ } else {
605
+ state.shuttingDown = true;
606
+ }
607
+ }
608
+ managedBrowser ??= managedBrowserForOwner(owner);
447
609
  if (options.awaitLaunch !== false) {
448
- await state.launchPromise?.catch(() => undefined);
449
- managedBrowser = managedBrowser ?? state.managedBrowser;
610
+ await launchPromiseForOwner(owner)?.catch(() => undefined);
611
+ managedBrowser ??= managedBrowserForOwner(owner);
450
612
  }
451
613
  if (!managedBrowser) return;
452
614
  if (managedBrowser.cleanupPromise) return managedBrowser.cleanupPromise;
453
615
 
454
- const cleanup = cleanupManagedBrowser(managedBrowser);
616
+ const cleanup = cleanupManagedBrowser(managedBrowser, owner);
455
617
  managedBrowser.cleanupPromise = cleanup;
456
618
  return cleanup;
457
619
  }
458
620
 
459
- async function cleanupManagedBrowser(managedBrowser: ManagedBrowser) {
460
- if (state.managedBrowser === managedBrowser) state.managedBrowser = undefined;
621
+ async function cleanupManagedBrowser(managedBrowser: ManagedBrowser, owner?: object) {
622
+ if (managedBrowserForOwner(owner) === managedBrowser) {
623
+ if (owner) invalidateWebMcpOperations(owner, "Chrome DevTools managed browser closed");
624
+ if (state.managedBrowser === managedBrowser) state.managedBrowser = undefined;
625
+ else setManagedBrowser(owner, undefined);
626
+ }
461
627
  if (!managedBrowser.exited) {
462
628
  killManagedBrowserProcess(managedBrowser);
463
629
  await waitForManagedBrowserExit(managedBrowser, BROWSER_SHUTDOWN_WAIT_MS).catch(async () => {
@@ -470,8 +636,8 @@ async function cleanupManagedBrowser(managedBrowser: ManagedBrowser) {
470
636
  await browserManagerOperations
471
637
  .rm(managedBrowser.userDataDir, { recursive: true, force: true })
472
638
  .catch(() => undefined);
473
- if (!state.portConfigured && managedBrowser.port === state.port) {
474
- state.port = state.configuredPort;
639
+ if (!state.portConfigured && managedBrowser.port === managedBrowserPort(owner)) {
640
+ setManagedBrowserPort(owner, state.configuredPort);
475
641
  }
476
642
  }
477
643
 
@@ -505,8 +671,8 @@ function waitForManagedBrowserExit(managedBrowser: ManagedBrowser, waitMs: numbe
505
671
  });
506
672
  }
507
673
 
508
- export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
509
- const url = `${devToolsEndpoint()}${path}`;
674
+ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit, owner?: object) {
675
+ const url = `${devToolsEndpoint(owner)}${path}`;
510
676
  let response: Response;
511
677
  try {
512
678
  const timeoutSignal = AbortSignal.timeout(DEFAULT_HTTP_TIMEOUT_MS);
@@ -515,7 +681,7 @@ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
515
681
  signal: init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal,
516
682
  });
517
683
  } catch (error) {
518
- throw new DevToolsEndpointError(endpointConnectionErrorMessage(error), {
684
+ throw new DevToolsEndpointError(endpointConnectionErrorMessage(error, owner), {
519
685
  launchable: true,
520
686
  retryable: true,
521
687
  });
@@ -545,9 +711,14 @@ export async function fetchDevToolsJson<T>(path: string, init?: RequestInit) {
545
711
  }
546
712
  }
547
713
 
548
- export async function withEndpointRetry<T>(operation: () => Promise<T>, waitMs: number) {
714
+ export async function withEndpointRetry<T>(
715
+ operation: () => Promise<T>,
716
+ waitMs: number,
717
+ signal?: AbortSignal,
718
+ ) {
549
719
  const deadline = Date.now() + waitMs;
550
720
  while (true) {
721
+ signal?.throwIfAborted();
551
722
  try {
552
723
  return await operation();
553
724
  } catch (error) {
@@ -556,7 +727,10 @@ export async function withEndpointRetry<T>(operation: () => Promise<T>, waitMs:
556
727
  const remainingMs = deadline - Date.now();
557
728
  if (remainingMs <= 0) throw error;
558
729
 
559
- await browserManagerOperations.sleep(Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs));
730
+ await browserManagerOperations.sleep(
731
+ Math.min(DEFAULT_ENDPOINT_RETRY_MS, remainingMs),
732
+ signal,
733
+ );
560
734
  }
561
735
  }
562
736
  }
@@ -586,10 +760,10 @@ function extensionsConfigured() {
586
760
  return state.extensionPaths.length > 0;
587
761
  }
588
762
 
589
- function endpointConnectionErrorMessage(error: unknown) {
763
+ function endpointConnectionErrorMessage(error: unknown, owner?: object) {
590
764
  const reason = isTimeoutError(error) ? "request timed out" : "connection failed";
591
765
  return [
592
- `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint()} (${reason}).`,
766
+ `Cannot connect to Chrome DevTools endpoint at ${devToolsEndpoint(owner)} (${reason}).`,
593
767
  launchHint(),
594
768
  endpointConfigHint(),
595
769
  ].join("\n");
@@ -599,8 +773,8 @@ function isTimeoutError(error: unknown) {
599
773
  return error instanceof Error && ["AbortError", "TimeoutError"].includes(error.name);
600
774
  }
601
775
 
602
- export function devToolsEndpoint() {
603
- return `http://${formatHostForUrl(state.host)}:${state.port}`;
776
+ export function devToolsEndpoint(owner?: object) {
777
+ return `http://${formatHostForUrl(state.host)}:${managedBrowserPort(owner)}`;
604
778
  }
605
779
 
606
780
  export function formatHostForUrl(host: string) {
@@ -614,19 +788,20 @@ export function endpointSourceLabel() {
614
788
 
615
789
  export type BrowserLifecycleState = "starting" | "running" | "exited" | "failed" | "unobserved";
616
790
 
617
- export function browserLifecycleState(): BrowserLifecycleState {
618
- if (state.launchPromise) return "starting";
619
- if (state.managedBrowser?.exited) return "exited";
620
- if (state.managedBrowser?.ready) return "running";
621
- if (state.lastLaunchAttempt?.lastError) return "failed";
791
+ export function browserLifecycleState(owner?: object): BrowserLifecycleState {
792
+ if (launchPromiseForOwner(owner)) return "starting";
793
+ if (managedBrowserForOwner(owner)?.exited) return "exited";
794
+ if (managedBrowserForOwner(owner)?.ready) return "running";
795
+ if (lastLaunchAttempt(owner)?.lastError) return "failed";
622
796
  return "unobserved";
623
797
  }
624
798
 
625
- export function launchModeLabel() {
799
+ export function launchModeLabel(owner?: object) {
800
+ const managedBrowser = managedBrowserForOwner(owner);
626
801
  if (extensionsConfigured()) {
627
802
  if (!isLocalDevToolsHost(state.host)) return "invalid; extensions require a local endpoint";
628
803
  if (!state.autoLaunchEnabled) return "invalid; extensions require auto-launch";
629
- if (state.managedBrowser && !state.managedBrowser.exited) {
804
+ if (managedBrowser && !managedBrowser.exited) {
630
805
  return state.portConfigured
631
806
  ? "extension-owned browser on explicit port"
632
807
  : "extension-owned browser on dynamic port";
@@ -637,7 +812,7 @@ export function launchModeLabel() {
637
812
  }
638
813
  if (!isLocalDevToolsHost(state.host)) return "manual remote endpoint";
639
814
  if (!state.autoLaunchEnabled) return "manual; auto-launch disabled";
640
- if (state.managedBrowser && !state.managedBrowser.exited) {
815
+ if (managedBrowser && !managedBrowser.exited) {
641
816
  return state.portConfigured
642
817
  ? "auto-launched on explicit port"
643
818
  : "auto-launched on dynamic port";
@@ -647,21 +822,18 @@ export function launchModeLabel() {
647
822
  : "attach first; auto-launch dynamic port";
648
823
  }
649
824
 
650
- export function launchAttemptLines() {
651
- if (!state.lastLaunchAttempt) return [];
825
+ export function launchAttemptLines(owner?: object) {
826
+ const attempt = lastLaunchAttempt(owner);
827
+ if (!attempt) return [];
652
828
 
653
- const lines = [`Last launch attempt: ${state.lastLaunchAttempt.mode}`];
654
- if (state.lastLaunchAttempt.selectedCandidate) {
655
- lines.push(`Launched browser: ${state.lastLaunchAttempt.selectedCandidate}`);
829
+ const lines = [`Last launch attempt: ${attempt.mode}`];
830
+ if (attempt.selectedCandidate) {
831
+ lines.push(`Launched browser: ${attempt.selectedCandidate}`);
656
832
  } else {
657
- lines.push(`Tried browser candidates: ${state.lastLaunchAttempt.candidateLabels.join(", ")}`);
658
- }
659
- if (state.lastLaunchAttempt.userDataDir) {
660
- lines.push(`Managed browser profile: ${state.lastLaunchAttempt.userDataDir}`);
661
- }
662
- if (state.lastLaunchAttempt.lastError) {
663
- lines.push(`Last launch error: ${state.lastLaunchAttempt.lastError}`);
833
+ lines.push(`Tried browser candidates: ${attempt.candidateLabels.join(", ")}`);
664
834
  }
835
+ if (attempt.userDataDir) lines.push(`Managed browser profile: ${attempt.userDataDir}`);
836
+ if (attempt.lastError) lines.push(`Last launch error: ${attempt.lastError}`);
665
837
  return lines;
666
838
  }
667
839
 
@@ -828,11 +1000,12 @@ async function resolveBrowserCandidates(
828
1000
  definitions: BrowserCandidateDefinition[],
829
1001
  generation: number,
830
1002
  signal: AbortSignal,
1003
+ owner?: object,
831
1004
  ) {
832
1005
  const candidates: BrowserCandidate[] = [];
833
1006
  for (const definition of definitions) {
834
1007
  const resolvedExecutable = await resolveBrowserExecutable(definition.executable);
835
- throwIfBrowserLaunchCancelled(generation, signal);
1008
+ throwIfBrowserLaunchCancelled(generation, signal, owner);
836
1009
  if (!resolvedExecutable) continue;
837
1010
  candidates.push({ ...definition, resolvedExecutable });
838
1011
  }