@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.d.ts +4 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +147 -540
- package/dist/bridge.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -0
- package/dist/cli.js.map +1 -1
- package/dist/mcp-types.d.ts +11 -0
- package/dist/mcp-types.d.ts.map +1 -1
- package/dist/overlays.d.ts +72 -0
- package/dist/overlays.d.ts.map +1 -0
- package/dist/overlays.js +323 -0
- package/dist/overlays.js.map +1 -0
- package/dist/profiles.d.ts +8 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +27 -0
- package/dist/profiles.js.map +1 -0
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +86 -6
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +405 -1
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +3 -83
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +3 -581
- package/dist/session.js.map +1 -1
- package/package.json +8 -7
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
|
|
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 {
|
|
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 (
|
|
56
|
-
nextOptions.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 =
|
|
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,
|
|
93
|
+
assertAuthSensitiveBrowserSupport(options.browser, managedUserDataDir);
|
|
96
94
|
}
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
111
|
-
let
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
203
|
+
lastState = toLocalBridgeState(await controller.bootstrapSession());
|
|
204
|
+
return lastState;
|
|
642
205
|
},
|
|
643
206
|
attachSession: async (requestedBrowserUrl) => {
|
|
644
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
296
|
+
return lastState.mode;
|
|
692
297
|
},
|
|
693
298
|
get presentationMode() {
|
|
694
|
-
|
|
299
|
+
lastState = toLocalBridgeState(controller.getState());
|
|
300
|
+
return lastState.presentationMode;
|
|
695
301
|
},
|
|
696
302
|
get preferredPresentationMode() {
|
|
697
|
-
|
|
303
|
+
lastState = toLocalBridgeState(controller.getState());
|
|
304
|
+
return lastState.preferredPresentationMode;
|
|
698
305
|
},
|
|
699
306
|
close: async () => {
|
|
700
307
|
input.removeListener("end", handleInputEnded);
|