@webmcp-bridge/local-mcp 0.5.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.
package/dist/bridge.js CHANGED
@@ -1,23 +1,13 @@
1
1
  /**
2
- * This module composes runtime startup with the stdio MCP server into one lifecycle handle.
3
- * It depends on site resolution, runtime startup, and session-control helpers so local-mcp can keep one MCP process alive across runtime, bootstrap, and attach transitions.
2
+ * This module composes site/runtime startup with the stdio MCP server into one lifecycle handle.
3
+ * It depends on agent-browser-core orchestration, site resolution, and server wiring so local-mcp stays a thin MCP facade over one browser session.
4
4
  */
5
+ import { assertAuthSensitiveBrowserSupport, resolveAuthPolicy, startBrowserSessionController, } from "@webmcp-bridge/agent-browser-core";
5
6
  import { createLocalMcpStdioServer, } from "./server.js";
6
- import { startLocalMcpRuntime, } from "./runtime.js";
7
- import { assertAuthSensitiveBrowserSupport, backupAndResetProfile, describeSessionStateFromAuth, ensureManagedProfile, findBrowserProcessForProfile, focusBrowserWindow, isProcessRunning, launchBootstrapBrowser, launchManagedAttachBrowser, readSessionMetadata, resolveAuthPolicy, stopBrowserProcess, stopManagedBrowser, updateSessionMetadata, waitForProcessExit, } from "./session.js";
7
+ import { resolveCdpConnectUrl, startLocalMcpRuntime, } from "./runtime.js";
8
+ import { evaluateDebugScript, evaluateOverlayTool, OverlayStore, } from "./overlays.js";
9
+ import { resolveDefaultUserDataDir } from "./profiles.js";
8
10
  import { createNativeSiteDefinition, resolveSiteSource, } from "./sites.js";
9
- const BOOTSTRAP_BROWSER_CLOSE_TIMEOUT_MS = 5_000;
10
- const BOOTSTRAP_PROFILE_RELEASE_DELAY_MS = 500;
11
- function readAuthState(value) {
12
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
13
- return "unknown";
14
- }
15
- const state = value.state;
16
- if (state === "authenticated" || state === "auth_required" || state === "challenge_required") {
17
- return state;
18
- }
19
- return "unknown";
20
- }
21
11
  async function resolveSiteDefinitionFromBridgeOptions(options) {
22
12
  const hasAdapterSource = Boolean(options.site || options.adapterModule);
23
13
  if (hasAdapterSource) {
@@ -41,7 +31,8 @@ async function resolveSiteDefinitionFromBridgeOptions(options) {
41
31
  async function startRuntime(options) {
42
32
  return await startLocalMcpRuntime(options);
43
33
  }
44
- function buildRuntimeStartOptions(baseOptions, siteDefinition, controlMode, preferredPresentationMode, browserUrl) {
34
+ function buildRuntimeStartOptions(baseOptions, siteDefinition, userDataDir, controlMode, preferredPresentationMode, browserUrl) {
35
+ const configuredBrowserUrl = baseOptions.browserUrl?.trim() || undefined;
45
36
  const nextOptions = {
46
37
  siteDefinition,
47
38
  preferredPresentationMode,
@@ -52,8 +43,8 @@ function buildRuntimeStartOptions(baseOptions, siteDefinition, controlMode, pref
52
43
  if (baseOptions.browser !== undefined) {
53
44
  nextOptions.browser = baseOptions.browser;
54
45
  }
55
- if (baseOptions.userDataDir !== undefined) {
56
- nextOptions.userDataDir = baseOptions.userDataDir;
46
+ if (userDataDir !== undefined) {
47
+ nextOptions.userDataDir = userDataDir;
57
48
  }
58
49
  if (baseOptions.preferNative !== undefined) {
59
50
  nextOptions.preferNative = baseOptions.preferNative;
@@ -65,7 +56,7 @@ function buildRuntimeStartOptions(baseOptions, siteDefinition, controlMode, pref
65
56
  if (!browserUrl) {
66
57
  throw new Error("CONFIG_ERROR: attach mode requires a browserUrl");
67
58
  }
68
- const explicitAttach = baseOptions.browserUrl !== undefined;
59
+ const explicitAttach = configuredBrowserUrl !== undefined;
69
60
  if (explicitAttach && baseOptions.browserChannel !== undefined) {
70
61
  throw new Error("CONFIG_ERROR: --browser-url cannot be combined with --browser-channel");
71
62
  }
@@ -84,6 +75,9 @@ function buildRuntimeStartOptions(baseOptions, siteDefinition, controlMode, pref
84
75
  }
85
76
  return nextOptions;
86
77
  }
78
+ function toLocalBridgeState(status) {
79
+ return status;
80
+ }
87
81
  export async function startLocalMcpBridge(options) {
88
82
  const siteDefinition = await resolveSiteDefinitionFromBridgeOptions(options);
89
83
  const authPolicy = resolveAuthPolicy(siteDefinition.manifest);
@@ -91,583 +85,194 @@ export async function startLocalMcpBridge(options) {
91
85
  if (!targetUrl) {
92
86
  throw new Error("CONFIG_ERROR: no target url provided (missing --url and manifest.defaultUrl)");
93
87
  }
88
+ const configuredBrowserUrl = options.browserUrl?.trim() || undefined;
89
+ const managedUserDataDir = configuredBrowserUrl === undefined
90
+ ? (options.userDataDir ?? resolveDefaultUserDataDir(siteDefinition, targetUrl))
91
+ : undefined;
94
92
  if (authPolicy.mode === "bootstrap_then_attach") {
95
- assertAuthSensitiveBrowserSupport(options.browser, options.userDataDir);
93
+ assertAuthSensitiveBrowserSupport(options.browser, managedUserDataDir);
96
94
  }
97
- let runtime;
98
- let runtimeMode = "control-only";
99
- let controlMode = "none";
100
- let ownership = "none";
101
- let authState = "unknown";
102
- let sessionState = authPolicy.mode === "bootstrap_then_attach" ? "profile_missing" : "runtime_active";
103
- let browserUrl = options.browserUrl;
104
- let browserPid;
105
- let preferredPresentationMode = options.preferredPresentationMode ?? "headed";
106
- let presentationMode = preferredPresentationMode;
107
- let lastBackupPath;
108
- let metadata;
95
+ const overlayStore = new OverlayStore(siteDefinition.id, managedUserDataDir);
96
+ await overlayStore.load();
109
97
  let server;
110
- let closed = false;
111
- let lifecycleTransition = Promise.resolve();
112
- const resourceUpdatedListeners = new Set();
113
- let unsubscribeRuntimeResourceUpdates;
114
- let ownerSessionGeneration = 0;
115
- const profilePath = options.userDataDir;
116
- const metadataFallback = profilePath
117
- ? {
118
- site: siteDefinition.id,
119
- targetUrl,
120
- authPolicy,
121
- }
122
- : undefined;
123
- const refreshStatus = () => {
124
- const state = {
125
- site: siteDefinition.id,
126
- targetUrl,
127
- controlMode,
128
- ...(browserUrl !== undefined ? { browserUrl } : {}),
129
- mode: runtimeMode,
130
- presentationMode,
131
- preferredPresentationMode,
132
- authPolicyMode: authPolicy.mode,
133
- authState,
134
- sessionState,
135
- ownership,
136
- ...(profilePath !== undefined ? { profilePath } : {}),
137
- ...(browserPid !== undefined ? { browserPid } : {}),
138
- ...(lastBackupPath !== undefined ? { lastBackupPath } : {}),
139
- };
140
- return state;
141
- };
142
- const syncFromMetadata = (nextMetadata) => {
143
- metadata = nextMetadata;
144
- controlMode = nextMetadata.controlMode;
145
- browserUrl = nextMetadata.browserUrl;
146
- browserPid = nextMetadata.browserPid;
147
- authState = nextMetadata.authState;
148
- sessionState = nextMetadata.sessionState;
149
- ownership = nextMetadata.ownership;
150
- presentationMode = nextMetadata.presentationMode;
151
- preferredPresentationMode = nextMetadata.preferredPresentationMode;
152
- if (nextMetadata.lastBackupPath !== undefined) {
153
- lastBackupPath = nextMetadata.lastBackupPath;
154
- }
155
- if (runtime === undefined) {
156
- runtimeMode = "control-only";
157
- presentationMode = nextMetadata.controlMode === "bootstrap" ? "headed" : nextMetadata.presentationMode;
158
- }
159
- };
160
- const writeMetadata = async (patch) => {
161
- if (!profilePath || !metadataFallback) {
162
- return;
163
- }
164
- const nextMetadata = await updateSessionMetadata(profilePath, metadataFallback, patch);
165
- syncFromMetadata(nextMetadata);
166
- };
167
- const hasRunningBootstrapBrowser = async (sourceMetadata) => {
168
- const activeMetadata = sourceMetadata ?? metadata;
169
- if (!activeMetadata) {
170
- return false;
171
- }
172
- if (authPolicy.mode !== "bootstrap_then_attach" ||
173
- activeMetadata.controlMode !== "bootstrap" ||
174
- activeMetadata.ownership !== "external") {
175
- return false;
176
- }
177
- if (await isProcessRunning(activeMetadata.browserPid)) {
178
- return true;
179
- }
180
- if (!profilePath) {
181
- return false;
182
- }
183
- const discoveredPid = await findBrowserProcessForProfile(profilePath);
184
- if (!discoveredPid) {
185
- return false;
186
- }
187
- if (discoveredPid !== activeMetadata.browserPid) {
188
- await writeMetadata({
189
- browserPid: discoveredPid,
190
- });
191
- }
192
- return true;
193
- };
194
- const bindRuntime = (nextRuntime, nextOwnership) => {
195
- runtime = nextRuntime;
196
- runtimeMode = nextRuntime.mode;
197
- controlMode = nextRuntime.controlMode;
198
- presentationMode = nextRuntime.presentationMode;
199
- ownership = nextOwnership;
200
- unsubscribeRuntimeResourceUpdates?.();
201
- unsubscribeRuntimeResourceUpdates = nextRuntime.gateway.onResourceUpdated((uri) => {
202
- for (const listener of resourceUpdatedListeners) {
203
- listener(uri);
98
+ let closeRequested = false;
99
+ let closeResources = async () => { };
100
+ const toolsetListeners = new Set();
101
+ const notifyToolsetMayHaveChanged = () => {
102
+ for (const listener of toolsetListeners) {
103
+ try {
104
+ listener();
204
105
  }
205
- });
206
- ownerSessionGeneration += 1;
207
- const generation = ownerSessionGeneration;
208
- void nextRuntime.ownerSessionEnded.then(() => {
209
- if (closed || generation !== ownerSessionGeneration) {
210
- return;
211
- }
212
- void closeResources().catch((error) => {
106
+ catch (error) {
213
107
  options.onError?.(error);
214
- });
215
- });
216
- };
217
- const clearRuntime = () => {
218
- runtime = undefined;
219
- runtimeMode = "control-only";
220
- unsubscribeRuntimeResourceUpdates?.();
221
- unsubscribeRuntimeResourceUpdates = undefined;
222
- };
223
- const closeRuntime = async () => {
224
- if (!runtime) {
225
- return;
108
+ }
226
109
  }
227
- const activeRuntime = runtime;
228
- clearRuntime();
229
- await activeRuntime.close();
230
110
  };
231
- const probeRuntimeAuthState = async (activeRuntime) => {
232
- if (!authPolicy.authProbeTool) {
233
- return "authenticated";
234
- }
235
- try {
236
- const result = await activeRuntime.gateway.callTool(authPolicy.authProbeTool, {});
237
- return readAuthState(result);
238
- }
239
- catch {
240
- return "unknown";
111
+ const requestClose = () => {
112
+ closeRequested = true;
113
+ if (server === undefined) {
114
+ return;
241
115
  }
116
+ void closeResources().catch((error) => {
117
+ options.onError?.(error);
118
+ });
242
119
  };
243
- const closeResourcesInternal = async () => {
120
+ const controller = await startBrowserSessionController({
121
+ site: siteDefinition.id,
122
+ targetUrl,
123
+ authPolicy,
124
+ ...(managedUserDataDir !== undefined ? { profilePath: managedUserDataDir } : {}),
125
+ ...(options.browserChannel !== undefined ? { browserChannel: options.browserChannel } : {}),
126
+ ...(options.browserUrl !== undefined ? { browserUrl: options.browserUrl } : {}),
127
+ ...(options.preferredPresentationMode !== undefined
128
+ ? { preferredPresentationMode: options.preferredPresentationMode }
129
+ : {}),
130
+ runtimeFactory: async ({ controlMode, preferredPresentationMode, browserUrl }) => await startRuntime(buildRuntimeStartOptions(options, siteDefinition, managedUserDataDir, controlMode, preferredPresentationMode, browserUrl)),
131
+ browserUrlHealthCheck: async (browserUrl) => {
132
+ await resolveCdpConnectUrl(browserUrl);
133
+ },
134
+ onCloseRequested: requestClose,
135
+ ...(options.onError !== undefined ? { onError: options.onError } : {}),
136
+ });
137
+ const unsubscribeControllerToolset = controller.onToolsetMayHaveChanged(() => {
138
+ notifyToolsetMayHaveChanged();
139
+ });
140
+ let closed = false;
141
+ let lastState = toLocalBridgeState(controller.getState());
142
+ closeResources = async () => {
244
143
  if (closed) {
245
144
  return;
246
145
  }
247
146
  closed = true;
248
- unsubscribeRuntimeResourceUpdates?.();
249
- const activeRuntime = runtime;
250
- clearRuntime();
251
- const results = await Promise.allSettled([server?.close(), activeRuntime?.close()]);
147
+ lastState = toLocalBridgeState(controller.getState());
148
+ unsubscribeControllerToolset();
149
+ const activeServer = server;
150
+ server = undefined;
151
+ const results = await Promise.allSettled([activeServer?.close(), controller.close()]);
252
152
  const firstFailure = results.find((result) => result.status === "rejected");
253
153
  if (firstFailure) {
254
154
  throw firstFailure.reason;
255
155
  }
256
156
  };
257
- const runLifecycleTransition = async (operation) => {
258
- const previousTransition = lifecycleTransition;
259
- let releaseTransition;
260
- lifecycleTransition = new Promise((resolve) => {
261
- releaseTransition = resolve;
262
- });
263
- await previousTransition.catch(() => {
264
- // Ignore previous transition failures so later lifecycle operations can still proceed.
265
- });
266
- try {
267
- return await operation();
268
- }
269
- finally {
270
- releaseTransition();
271
- }
272
- };
273
- const closeResources = async () => {
274
- await runLifecycleTransition(async () => {
275
- await closeResourcesInternal();
276
- });
277
- };
278
- const bootstrapSessionInternal = async (nextAuthState = "unknown") => {
279
- if (authPolicy.mode !== "bootstrap_then_attach" || !profilePath) {
280
- throw new Error("UNSUPPORTED_SESSION_CONTROL: bootstrap is available only for auth-sensitive managed sessions");
281
- }
282
- await closeRuntime();
283
- if (await hasRunningBootstrapBrowser()) {
284
- return refreshStatus();
285
- }
286
- if (metadata?.ownership === "managed") {
287
- await stopManagedBrowser(metadata);
288
- }
289
- await ensureManagedProfile(profilePath);
290
- const bootstrapOptions = {
291
- targetUrl,
292
- userDataDir: profilePath,
293
- };
294
- if (options.browserChannel !== undefined) {
295
- bootstrapOptions.browserChannel = options.browserChannel;
296
- }
297
- const launchResult = await launchBootstrapBrowser(bootstrapOptions);
298
- const bootstrapPatch = {
299
- presentationMode: "headed",
300
- preferredPresentationMode,
301
- sessionState: nextAuthState === "unknown" ? "bootstrap_active" : describeSessionStateFromAuth(nextAuthState),
302
- authState: nextAuthState,
303
- controlMode: "bootstrap",
304
- ownership: "external",
305
- browserUrl: null,
306
- browserPid: null,
307
- };
308
- if (launchResult.pid !== undefined) {
309
- bootstrapPatch.browserPid = launchResult.pid;
310
- }
311
- await writeMetadata(bootstrapPatch);
312
- return refreshStatus();
313
- };
314
- const adoptManagedAttachBrowserAsBootstrap = async (nextAuthState, nextBrowserPid) => {
315
- await closeRuntime();
316
- const bootstrapPatch = {
317
- presentationMode: "headed",
318
- preferredPresentationMode: "headed",
319
- sessionState: describeSessionStateFromAuth(nextAuthState),
320
- authState: nextAuthState,
321
- controlMode: "bootstrap",
322
- ownership: "external",
323
- browserUrl: null,
324
- browserPid: null,
325
- };
326
- if (nextBrowserPid !== undefined) {
327
- bootstrapPatch.browserPid = nextBrowserPid;
328
- }
329
- preferredPresentationMode = "headed";
330
- await writeMetadata(bootstrapPatch);
331
- return refreshStatus();
332
- };
333
- const activateRuntime = async (nextRuntime, nextOwnership, nextBrowserUrl, nextBrowserPid) => {
334
- const nextAuthState = await probeRuntimeAuthState(nextRuntime);
335
- bindRuntime(nextRuntime, nextOwnership);
336
- browserUrl = nextBrowserUrl;
337
- browserPid = nextBrowserPid;
338
- authState = nextAuthState;
339
- sessionState = "runtime_active";
340
- presentationMode = nextRuntime.presentationMode;
341
- if (profilePath && metadataFallback) {
342
- const runtimePatch = {
343
- presentationMode: nextRuntime.presentationMode,
344
- preferredPresentationMode,
345
- sessionState: "runtime_active",
346
- authState: nextAuthState,
347
- controlMode: nextRuntime.controlMode,
348
- ownership: nextOwnership,
349
- };
350
- if (nextBrowserUrl !== undefined) {
351
- runtimePatch.browserUrl = nextBrowserUrl;
352
- }
353
- else {
354
- runtimePatch.browserUrl = null;
355
- }
356
- if (nextBrowserPid !== undefined) {
357
- runtimePatch.browserPid = nextBrowserPid;
358
- }
359
- else {
360
- runtimePatch.browserPid = null;
361
- }
362
- await writeMetadata(runtimePatch);
363
- }
364
- return refreshStatus();
365
- };
366
- const attachSessionInternal = async (requestedBrowserUrl, requestedPresentationMode = preferredPresentationMode) => {
367
- const explicitBrowserUrl = requestedBrowserUrl?.trim() || options.browserUrl?.trim();
368
- const relaunchManagedAttachBrowser = !explicitBrowserUrl &&
369
- controlMode === "attach" &&
370
- ownership === "managed" &&
371
- requestedPresentationMode !== presentationMode;
372
- const activeBrowserUrl = explicitBrowserUrl || (relaunchManagedAttachBrowser ? undefined : browserUrl);
373
- const nextOwnership = explicitBrowserUrl ? "external" : "managed";
374
- if (runtime) {
375
- await closeRuntime();
376
- }
377
- if (relaunchManagedAttachBrowser) {
378
- const managedBrowserPid = browserPid ?? (profilePath ? await findBrowserProcessForProfile(profilePath) : undefined);
379
- if (profilePath && metadataFallback) {
380
- await updateSessionMetadata(profilePath, metadataFallback, {
381
- controlMode: "none",
382
- ownership: "none",
383
- browserUrl: null,
384
- browserPid: null,
385
- });
386
- }
387
- await stopBrowserProcess(managedBrowserPid);
388
- if (managedBrowserPid) {
389
- const didExit = await waitForProcessExit(managedBrowserPid, BOOTSTRAP_BROWSER_CLOSE_TIMEOUT_MS);
390
- if (!didExit) {
391
- throw new Error(`BROWSER_CLOSE_TIMEOUT: timed out waiting for managed browser ${String(managedBrowserPid)} to exit`);
392
- }
393
- await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_PROFILE_RELEASE_DELAY_MS));
394
- }
395
- browserUrl = undefined;
396
- browserPid = undefined;
397
- }
398
- let managedAttachPid;
399
- let attachBrowserUrl = activeBrowserUrl;
400
- if (!attachBrowserUrl) {
401
- if (authPolicy.mode !== "bootstrap_then_attach" || !profilePath) {
402
- throw new Error("CONFIG_ERROR: bridge.session.attach requires browserUrl when no managed attach session exists");
403
- }
404
- if (!explicitBrowserUrl && (await hasRunningBootstrapBrowser())) {
405
- const bootstrapPid = metadata?.browserPid ?? (profilePath ? await findBrowserProcessForProfile(profilePath) : undefined);
406
- await stopBrowserProcess(bootstrapPid);
407
- const didExit = await waitForProcessExit(bootstrapPid, BOOTSTRAP_BROWSER_CLOSE_TIMEOUT_MS);
408
- if (!didExit) {
409
- throw new Error(`BOOTSTRAP_BROWSER_CLOSE_TIMEOUT: timed out waiting for bootstrap browser ${String(bootstrapPid)} to exit`);
410
- }
411
- await writeMetadata({
412
- controlMode: "none",
413
- ownership: "none",
414
- browserUrl: null,
415
- browserPid: null,
416
- });
417
- await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_PROFILE_RELEASE_DELAY_MS));
418
- }
419
- const existingProfileBrowserPid = await findBrowserProcessForProfile(profilePath);
420
- if (existingProfileBrowserPid) {
421
- await stopBrowserProcess(existingProfileBrowserPid);
422
- const didExit = await waitForProcessExit(existingProfileBrowserPid, BOOTSTRAP_BROWSER_CLOSE_TIMEOUT_MS);
423
- if (!didExit) {
424
- throw new Error(`BROWSER_CLOSE_TIMEOUT: timed out waiting for existing browser ${String(existingProfileBrowserPid)} to exit`);
425
- }
426
- await new Promise((resolve) => setTimeout(resolve, BOOTSTRAP_PROFILE_RELEASE_DELAY_MS));
427
- }
428
- await ensureManagedProfile(profilePath);
429
- const attachOptions = {
430
- targetUrl,
431
- userDataDir: profilePath,
432
- presentationMode: requestedPresentationMode,
433
- };
434
- if (options.browserChannel !== undefined) {
435
- attachOptions.browserChannel = options.browserChannel;
436
- }
437
- const managedAttach = await launchManagedAttachBrowser(attachOptions);
438
- attachBrowserUrl = managedAttach.browserUrl;
439
- managedAttachPid = managedAttach.pid;
440
- }
441
- try {
442
- const nextRuntime = await startRuntime(buildRuntimeStartOptions(options, siteDefinition, "attach", requestedPresentationMode, attachBrowserUrl));
443
- const nextAuthState = await probeRuntimeAuthState(nextRuntime);
444
- if (authPolicy.mode === "bootstrap_then_attach" &&
445
- !explicitBrowserUrl &&
446
- (nextAuthState === "auth_required" || nextAuthState === "challenge_required")) {
447
- if (managedAttachPid && requestedPresentationMode === "headed") {
448
- bindRuntime(nextRuntime, "managed");
449
- return await adoptManagedAttachBrowserAsBootstrap(nextAuthState, managedAttachPid);
450
- }
451
- await nextRuntime.close();
452
- return await bootstrapSessionInternal(nextAuthState);
453
- }
454
- preferredPresentationMode = requestedPresentationMode;
455
- return await activateRuntime(nextRuntime, nextOwnership, attachBrowserUrl, managedAttachPid ?? browserPid);
456
- }
457
- catch (error) {
458
- if (managedAttachPid && profilePath && metadataFallback) {
459
- const cleanupMetadata = await updateSessionMetadata(profilePath, metadataFallback, {
460
- controlMode: "none",
461
- ownership: "none",
462
- browserUrl: null,
463
- browserPid: null,
464
- });
465
- await stopManagedBrowser({
466
- ...cleanupMetadata,
467
- ownership: "managed",
468
- browserPid: managedAttachPid,
469
- });
470
- }
471
- await closeResourcesInternal().catch(options.onError);
472
- throw error;
473
- }
474
- };
475
- const setPresentationModeInternal = async (setModeOptions) => {
476
- if (closed) {
477
- throw new Error("SESSION_NOT_AVAILABLE: local-mcp bridge session is closed");
478
- }
479
- const requestedPresentationMode = setModeOptions.presentationMode;
480
- const previousRuntime = runtime;
481
- const previousState = refreshStatus();
482
- const previousPreferredPresentationMode = preferredPresentationMode;
483
- if (controlMode === "bootstrap") {
484
- throw new Error("UNSUPPORTED_SESSION_CONTROL: bridge.session.mode.set is unavailable while the bridge is in bootstrap mode");
485
- }
486
- if (ownership === "external") {
487
- throw new Error("UNSUPPORTED_SESSION_CONTROL: bridge.session.mode.set is unavailable for external attach sessions");
488
- }
489
- if (requestedPresentationMode === presentationMode) {
490
- preferredPresentationMode = requestedPresentationMode;
491
- if (profilePath && metadataFallback) {
492
- await writeMetadata({
493
- presentationMode,
494
- preferredPresentationMode,
495
- });
496
- }
497
- return refreshStatus();
498
- }
499
- if (controlMode === "attach") {
500
- preferredPresentationMode = requestedPresentationMode;
501
- return await attachSessionInternal(undefined, requestedPresentationMode);
502
- }
503
- await closeRuntime();
504
- preferredPresentationMode = requestedPresentationMode;
505
- try {
506
- const nextRuntime = await startRuntime(buildRuntimeStartOptions(options, siteDefinition, "launch", requestedPresentationMode));
507
- return await activateRuntime(nextRuntime, "managed");
508
- }
509
- catch (error) {
510
- preferredPresentationMode = previousPreferredPresentationMode;
511
- if (previousRuntime) {
512
- try {
513
- const recoveredRuntime = await startRuntime(buildRuntimeStartOptions(options, siteDefinition, "launch", previousState.presentationMode));
514
- await activateRuntime(recoveredRuntime, previousState.ownership);
515
- }
516
- catch (recoveryError) {
517
- options.onError?.(recoveryError);
518
- await closeResourcesInternal().catch(options.onError);
519
- }
520
- }
521
- else {
522
- await closeResourcesInternal().catch(options.onError);
523
- }
524
- throw error;
525
- }
526
- };
527
- const setPresentationMode = async (setModeOptions) => {
528
- return await runLifecycleTransition(async () => await setPresentationModeInternal(setModeOptions));
529
- };
530
- const resetProfileInternal = async () => {
531
- if (!profilePath || !metadataFallback) {
532
- throw new Error("UNSUPPORTED_SESSION_CONTROL: reset_profile requires a managed --user-data-dir");
533
- }
534
- await closeRuntime();
535
- if (metadata?.ownership === "managed") {
536
- await stopManagedBrowser(metadata);
537
- }
538
- const resetResult = await backupAndResetProfile(profilePath, metadataFallback);
539
- syncFromMetadata(resetResult.metadata);
540
- lastBackupPath = resetResult.backupPath;
541
- if (authPolicy.mode === "bootstrap_then_attach") {
542
- return await bootstrapSessionInternal();
543
- }
544
- return refreshStatus();
545
- };
546
- const initializeControlPlane = async () => {
547
- if (authPolicy.mode !== "bootstrap_then_attach") {
548
- const nextControlMode = options.browserUrl ? "attach" : "launch";
549
- const nextRuntime = await startRuntime(buildRuntimeStartOptions(options, siteDefinition, nextControlMode, preferredPresentationMode, options.browserUrl));
550
- await activateRuntime(nextRuntime, nextControlMode === "attach" ? "external" : "managed", options.browserUrl);
551
- return;
552
- }
553
- const managedProfilePath = profilePath;
554
- metadata = await readSessionMetadata(managedProfilePath, metadataFallback);
555
- syncFromMetadata(metadata);
556
- if (options.browserUrl) {
557
- await attachSessionInternal(options.browserUrl);
558
- return;
559
- }
560
- const hasRunningBootstrapExternal = await hasRunningBootstrapBrowser(metadata);
561
- if (hasRunningBootstrapExternal) {
562
- return;
563
- }
564
- const hasRunningManagedAttach = metadata.controlMode === "attach" &&
565
- metadata.browserUrl !== undefined &&
566
- metadata.ownership === "managed" &&
567
- (await isProcessRunning(metadata.browserPid));
568
- if (hasRunningManagedAttach) {
569
- await attachSessionInternal();
570
- return;
571
- }
572
- if (metadata.authState === "authenticated") {
573
- await attachSessionInternal();
574
- return;
575
- }
576
- if (metadata.sessionState !== "profile_missing") {
577
- await attachSessionInternal();
578
- return;
579
- }
580
- await bootstrapSessionInternal(metadata.authState === "auth_required" || metadata.authState === "challenge_required"
581
- ? metadata.authState
582
- : "unknown");
583
- };
584
- await initializeControlPlane();
585
157
  const gateway = {
586
158
  listTools: async () => {
159
+ const runtime = controller.getRuntime();
587
160
  if (!runtime) {
588
161
  return [];
589
162
  }
590
- return await runtime.gateway.listTools();
163
+ const pageTools = await runtime.gateway.listTools();
164
+ return [...pageTools, ...overlayStore.listEnabledToolDefinitions()];
591
165
  },
592
166
  callTool: async (name, input) => {
167
+ const runtime = controller.getRuntime();
593
168
  if (!runtime) {
594
169
  throw new Error("SESSION_NOT_AVAILABLE: page tools are unavailable while local-mcp is waiting for bootstrap or attach");
595
170
  }
596
- return await runtime.gateway.callTool(name, input);
171
+ const overlayTool = overlayStore.getOverlayTool(name);
172
+ if (overlayTool) {
173
+ return await evaluateOverlayTool(runtime.page, overlayTool.overlay, overlayTool.tool, input);
174
+ }
175
+ return (await runtime.gateway.callTool(name, input));
597
176
  },
598
177
  listResources: async () => {
178
+ const runtime = controller.getRuntime();
599
179
  if (!runtime) {
600
180
  return [];
601
181
  }
602
182
  return await runtime.gateway.listResources();
603
183
  },
604
184
  readResource: async (uri) => {
185
+ const runtime = controller.getRuntime();
605
186
  if (!runtime) {
606
187
  throw new Error(`RESOURCE_NOT_FOUND: ${uri}`);
607
188
  }
608
189
  return await runtime.gateway.readResource(uri);
609
190
  },
610
- onResourceUpdated: (listener) => {
611
- resourceUpdatedListeners.add(listener);
612
- return () => {
613
- resourceUpdatedListeners.delete(listener);
614
- };
615
- },
191
+ onResourceUpdated: (listener) => controller.onResourceUpdated(listener),
616
192
  };
617
193
  try {
618
194
  const serverOptions = {
619
195
  gateway,
620
196
  bridgeControl: {
621
- getState: refreshStatus,
622
- openWindow: async () => {
623
- if (runtime) {
624
- return await runtime.openWindow();
625
- }
626
- if (authPolicy.mode === "bootstrap_then_attach") {
627
- if (await hasRunningBootstrapBrowser()) {
628
- await focusBrowserWindow(options.browserChannel).catch(() => {
629
- // Focusing an external browser is best-effort; reuse still avoids duplicate windows.
630
- });
631
- return "focused";
632
- }
633
- await runLifecycleTransition(async () => {
634
- await bootstrapSessionInternal(authState);
635
- });
636
- return "opened";
637
- }
638
- throw new Error("SESSION_NOT_AVAILABLE: current page is closed");
197
+ getState: () => {
198
+ lastState = toLocalBridgeState(controller.getState());
199
+ return lastState;
639
200
  },
201
+ openWindow: async () => await controller.openWindow(),
640
202
  bootstrapSession: async () => {
641
- return await runLifecycleTransition(async () => await bootstrapSessionInternal(authState));
203
+ lastState = toLocalBridgeState(await controller.bootstrapSession());
204
+ return lastState;
642
205
  },
643
206
  attachSession: async (requestedBrowserUrl) => {
644
- return await runLifecycleTransition(async () => await attachSessionInternal(requestedBrowserUrl));
207
+ lastState = toLocalBridgeState(await controller.attachSession(requestedBrowserUrl));
208
+ return lastState;
209
+ },
210
+ debugEval: async (script, args) => {
211
+ const runtime = controller.getRuntime();
212
+ if (!runtime) {
213
+ throw new Error("SESSION_NOT_AVAILABLE: debug eval requires an active browser runtime");
214
+ }
215
+ return await evaluateDebugScript(runtime.page, script, args);
216
+ },
217
+ listOverlays: async () => overlayStore.list(),
218
+ installOverlay: async (installOptions) => {
219
+ const overlay = await overlayStore.install(installOptions);
220
+ notifyToolsetMayHaveChanged();
221
+ return overlay;
222
+ },
223
+ updateOverlay: async (updateOptions) => {
224
+ const overlay = await overlayStore.update(updateOptions);
225
+ notifyToolsetMayHaveChanged();
226
+ return overlay;
227
+ },
228
+ enableOverlay: async (id) => {
229
+ const overlay = await overlayStore.enable(id);
230
+ notifyToolsetMayHaveChanged();
231
+ return overlay;
232
+ },
233
+ disableOverlay: async (id) => {
234
+ const overlay = await overlayStore.disable(id);
235
+ notifyToolsetMayHaveChanged();
236
+ return overlay;
237
+ },
238
+ deleteOverlay: async (id) => {
239
+ await overlayStore.delete(id);
240
+ notifyToolsetMayHaveChanged();
241
+ },
242
+ getPresentationMode: () => controller.getPresentationMode(),
243
+ setPresentationMode: async (setModeOptions) => {
244
+ lastState = toLocalBridgeState(await controller.setPresentationMode(setModeOptions));
245
+ return lastState;
645
246
  },
646
- getPresentationMode: () => refreshStatus().presentationMode,
647
- setPresentationMode: async (setModeOptions) => await setPresentationMode(setModeOptions),
648
247
  resetProfile: async () => {
649
- return await runLifecycleTransition(async () => await resetProfileInternal());
248
+ lastState = toLocalBridgeState(await controller.resetProfile());
249
+ await overlayStore.load();
250
+ notifyToolsetMayHaveChanged();
251
+ return lastState;
650
252
  },
651
253
  closeBridge: async () => {
652
254
  await closeResources();
653
255
  },
654
256
  },
655
257
  serviceVersion: options.serviceVersion,
258
+ onToolsetMayHaveChanged: (listener) => {
259
+ toolsetListeners.add(listener);
260
+ return () => {
261
+ toolsetListeners.delete(listener);
262
+ };
263
+ },
264
+ ...(options.input !== undefined ? { input: options.input } : {}),
265
+ ...(options.output !== undefined ? { output: options.output } : {}),
266
+ ...(options.onError !== undefined ? { onError: options.onError } : {}),
656
267
  };
657
- if (options.input !== undefined) {
658
- serverOptions.input = options.input;
659
- }
660
- if (options.output !== undefined) {
661
- serverOptions.output = options.output;
662
- }
663
- if (options.onError !== undefined) {
664
- serverOptions.onError = options.onError;
665
- }
666
268
  server = createLocalMcpStdioServer(serverOptions);
667
269
  await server.start();
270
+ if (closeRequested) {
271
+ await closeResources();
272
+ }
668
273
  }
669
274
  catch (error) {
670
- await closeResourcesInternal().catch(options.onError);
275
+ await closeResources().catch(options.onError);
671
276
  throw error;
672
277
  }
673
278
  const input = options.input ?? process.stdin;
@@ -679,22 +284,24 @@ export async function startLocalMcpBridge(options) {
679
284
  input.once("end", handleInputEnded);
680
285
  return {
681
286
  get site() {
682
- return siteDefinition.id;
287
+ return lastState.site;
683
288
  },
684
289
  get targetUrl() {
685
- return targetUrl;
290
+ return lastState.targetUrl;
686
291
  },
687
292
  get controlMode() {
688
- return controlMode;
293
+ return lastState.controlMode;
689
294
  },
690
295
  get mode() {
691
- return runtimeMode;
296
+ return lastState.mode;
692
297
  },
693
298
  get presentationMode() {
694
- return presentationMode;
299
+ lastState = toLocalBridgeState(controller.getState());
300
+ return lastState.presentationMode;
695
301
  },
696
302
  get preferredPresentationMode() {
697
- return preferredPresentationMode;
303
+ lastState = toLocalBridgeState(controller.getState());
304
+ return lastState.preferredPresentationMode;
698
305
  },
699
306
  close: async () => {
700
307
  input.removeListener("end", handleInputEnded);