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