@webmcp-bridge/local-mcp 0.4.3 → 0.5.1
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 +10 -5
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +620 -70
- package/dist/bridge.js.map +1 -1
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +30 -8
- package/dist/cli.js.map +1 -1
- package/dist/mcp-types.d.ts +23 -1
- package/dist/mcp-types.d.ts.map +1 -1
- package/dist/runtime.d.ts +15 -4
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +318 -28
- package/dist/runtime.js.map +1 -1
- package/dist/server.d.ts +30 -7
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +283 -16
- package/dist/server.js.map +1 -1
- package/dist/session.d.ts +86 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +584 -0
- package/dist/session.js.map +1 -0
- package/dist/sites.d.ts +1 -1
- package/dist/sites.d.ts.map +1 -1
- package/dist/sites.js +50 -0
- package/dist/sites.js.map +1 -1
- package/package.json +8 -6
- package/dist/client.d.ts +0 -39
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -232
- package/dist/client.js.map +0 -1
- package/dist/event-buffer.d.ts +0 -22
- package/dist/event-buffer.d.ts.map +0 -1
- package/dist/event-buffer.js +0 -46
- package/dist/event-buffer.js.map +0 -1
- package/dist/protocol.d.ts +0 -11
- package/dist/protocol.d.ts.map +0 -1
- package/dist/protocol.js +0 -52
- package/dist/protocol.js.map +0 -1
package/dist/bridge.js
CHANGED
|
@@ -1,45 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* This module composes runtime startup with the stdio MCP server into one lifecycle handle.
|
|
3
|
-
* It depends on site
|
|
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.
|
|
4
4
|
*/
|
|
5
5
|
import { createLocalMcpStdioServer, } from "./server.js";
|
|
6
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
8
|
import { createNativeSiteDefinition, resolveSiteSource, } from "./sites.js";
|
|
9
|
+
const BOOTSTRAP_BROWSER_CLOSE_TIMEOUT_MS = 5_000;
|
|
10
|
+
const BOOTSTRAP_PROFILE_RELEASE_DELAY_MS = 500;
|
|
8
11
|
function readAuthState(value) {
|
|
9
12
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
10
|
-
return
|
|
13
|
+
return "unknown";
|
|
11
14
|
}
|
|
12
15
|
const state = value.state;
|
|
13
16
|
if (state === "authenticated" || state === "auth_required" || state === "challenge_required") {
|
|
14
17
|
return state;
|
|
15
18
|
}
|
|
16
|
-
return
|
|
19
|
+
return "unknown";
|
|
17
20
|
}
|
|
18
|
-
async function
|
|
19
|
-
const runtimeOptions = {
|
|
20
|
-
siteDefinition,
|
|
21
|
-
headless,
|
|
22
|
-
};
|
|
23
|
-
if (options.url !== undefined) {
|
|
24
|
-
runtimeOptions.url = options.url;
|
|
25
|
-
}
|
|
26
|
-
if (options.browser !== undefined) {
|
|
27
|
-
runtimeOptions.browser = options.browser;
|
|
28
|
-
}
|
|
29
|
-
if (options.userDataDir !== undefined) {
|
|
30
|
-
runtimeOptions.userDataDir = options.userDataDir;
|
|
31
|
-
}
|
|
32
|
-
if (options.browserChannel !== undefined) {
|
|
33
|
-
runtimeOptions.browserChannel = options.browserChannel;
|
|
34
|
-
}
|
|
35
|
-
if (options.preferNative !== undefined) {
|
|
36
|
-
runtimeOptions.preferNative = options.preferNative;
|
|
37
|
-
}
|
|
38
|
-
return await startLocalMcpRuntime(runtimeOptions);
|
|
39
|
-
}
|
|
40
|
-
async function resolveRuntime(options) {
|
|
21
|
+
async function resolveSiteDefinitionFromBridgeOptions(options) {
|
|
41
22
|
const hasAdapterSource = Boolean(options.site || options.adapterModule);
|
|
42
|
-
let siteDefinition;
|
|
43
23
|
if (hasAdapterSource) {
|
|
44
24
|
const sourceOptions = {};
|
|
45
25
|
if (options.site !== undefined) {
|
|
@@ -51,61 +31,623 @@ async function resolveRuntime(options) {
|
|
|
51
31
|
if (options.moduleBaseDir !== undefined) {
|
|
52
32
|
sourceOptions.moduleBaseDir = options.moduleBaseDir;
|
|
53
33
|
}
|
|
54
|
-
|
|
34
|
+
return await resolveSiteSource(sourceOptions);
|
|
55
35
|
}
|
|
56
|
-
|
|
57
|
-
|
|
36
|
+
if (options.url) {
|
|
37
|
+
return createNativeSiteDefinition(options.url);
|
|
58
38
|
}
|
|
59
|
-
|
|
60
|
-
|
|
39
|
+
throw new Error("CONFIG_ERROR: provide --url or one of --site/--adapter-module");
|
|
40
|
+
}
|
|
41
|
+
async function startRuntime(options) {
|
|
42
|
+
return await startLocalMcpRuntime(options);
|
|
43
|
+
}
|
|
44
|
+
function buildRuntimeStartOptions(baseOptions, siteDefinition, controlMode, preferredPresentationMode, browserUrl) {
|
|
45
|
+
const nextOptions = {
|
|
46
|
+
siteDefinition,
|
|
47
|
+
preferredPresentationMode,
|
|
48
|
+
};
|
|
49
|
+
if (baseOptions.url !== undefined) {
|
|
50
|
+
nextOptions.url = baseOptions.url;
|
|
61
51
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const autoLoginFallback = options.autoLoginFallback ?? true;
|
|
65
|
-
const authProbeTool = siteDefinition.manifest.authProbeTool;
|
|
66
|
-
if (!autoLoginFallback || !requestedHeadless || !authProbeTool) {
|
|
67
|
-
return primary;
|
|
52
|
+
if (baseOptions.browser !== undefined) {
|
|
53
|
+
nextOptions.browser = baseOptions.browser;
|
|
68
54
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
if (baseOptions.userDataDir !== undefined) {
|
|
56
|
+
nextOptions.userDataDir = baseOptions.userDataDir;
|
|
57
|
+
}
|
|
58
|
+
if (baseOptions.preferNative !== undefined) {
|
|
59
|
+
nextOptions.preferNative = baseOptions.preferNative;
|
|
60
|
+
}
|
|
61
|
+
if (baseOptions.autoLoginFallback !== undefined) {
|
|
62
|
+
nextOptions.autoLoginFallback = baseOptions.autoLoginFallback;
|
|
63
|
+
}
|
|
64
|
+
if (controlMode === "attach") {
|
|
65
|
+
if (!browserUrl) {
|
|
66
|
+
throw new Error("CONFIG_ERROR: attach mode requires a browserUrl");
|
|
74
67
|
}
|
|
68
|
+
const explicitAttach = baseOptions.browserUrl !== undefined;
|
|
69
|
+
if (explicitAttach && baseOptions.browserChannel !== undefined) {
|
|
70
|
+
throw new Error("CONFIG_ERROR: --browser-url cannot be combined with --browser-channel");
|
|
71
|
+
}
|
|
72
|
+
if (explicitAttach && baseOptions.chromiumLoginWorkaround !== undefined) {
|
|
73
|
+
throw new Error("CONFIG_ERROR: --browser-url cannot be combined with --chromium-login-workaround");
|
|
74
|
+
}
|
|
75
|
+
nextOptions.browserUrl = browserUrl;
|
|
76
|
+
nextOptions.browserUrlOrigin = explicitAttach ? "external" : "managed";
|
|
77
|
+
return nextOptions;
|
|
75
78
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return primary;
|
|
79
|
+
if (baseOptions.browserChannel !== undefined) {
|
|
80
|
+
nextOptions.browserChannel = baseOptions.browserChannel;
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
if (baseOptions.chromiumLoginWorkaround !== undefined) {
|
|
83
|
+
nextOptions.chromiumLoginWorkaround = baseOptions.chromiumLoginWorkaround;
|
|
84
|
+
}
|
|
85
|
+
return nextOptions;
|
|
82
86
|
}
|
|
83
87
|
export async function startLocalMcpBridge(options) {
|
|
84
|
-
const
|
|
88
|
+
const siteDefinition = await resolveSiteDefinitionFromBridgeOptions(options);
|
|
89
|
+
const authPolicy = resolveAuthPolicy(siteDefinition.manifest);
|
|
90
|
+
const targetUrl = options.url?.trim() || siteDefinition.manifest.defaultUrl?.trim() || "";
|
|
91
|
+
if (!targetUrl) {
|
|
92
|
+
throw new Error("CONFIG_ERROR: no target url provided (missing --url and manifest.defaultUrl)");
|
|
93
|
+
}
|
|
94
|
+
if (authPolicy.mode === "bootstrap_then_attach") {
|
|
95
|
+
assertAuthSensitiveBrowserSupport(options.browser, options.userDataDir);
|
|
96
|
+
}
|
|
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;
|
|
85
109
|
let server;
|
|
86
110
|
let closed = false;
|
|
87
|
-
|
|
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);
|
|
204
|
+
}
|
|
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) => {
|
|
213
|
+
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;
|
|
226
|
+
}
|
|
227
|
+
const activeRuntime = runtime;
|
|
228
|
+
clearRuntime();
|
|
229
|
+
await activeRuntime.close();
|
|
230
|
+
};
|
|
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";
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
const closeResourcesInternal = async () => {
|
|
88
244
|
if (closed) {
|
|
89
245
|
return;
|
|
90
246
|
}
|
|
91
247
|
closed = true;
|
|
92
|
-
|
|
248
|
+
unsubscribeRuntimeResourceUpdates?.();
|
|
249
|
+
const activeRuntime = runtime;
|
|
250
|
+
clearRuntime();
|
|
251
|
+
const results = await Promise.allSettled([server?.close(), activeRuntime?.close()]);
|
|
93
252
|
const firstFailure = results.find((result) => result.status === "rejected");
|
|
94
253
|
if (firstFailure) {
|
|
95
254
|
throw firstFailure.reason;
|
|
96
255
|
}
|
|
97
256
|
};
|
|
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
|
+
const gateway = {
|
|
586
|
+
listTools: async () => {
|
|
587
|
+
if (!runtime) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
return await runtime.gateway.listTools();
|
|
591
|
+
},
|
|
592
|
+
callTool: async (name, input) => {
|
|
593
|
+
if (!runtime) {
|
|
594
|
+
throw new Error("SESSION_NOT_AVAILABLE: page tools are unavailable while local-mcp is waiting for bootstrap or attach");
|
|
595
|
+
}
|
|
596
|
+
return await runtime.gateway.callTool(name, input);
|
|
597
|
+
},
|
|
598
|
+
listResources: async () => {
|
|
599
|
+
if (!runtime) {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
return await runtime.gateway.listResources();
|
|
603
|
+
},
|
|
604
|
+
readResource: async (uri) => {
|
|
605
|
+
if (!runtime) {
|
|
606
|
+
throw new Error(`RESOURCE_NOT_FOUND: ${uri}`);
|
|
607
|
+
}
|
|
608
|
+
return await runtime.gateway.readResource(uri);
|
|
609
|
+
},
|
|
610
|
+
onResourceUpdated: (listener) => {
|
|
611
|
+
resourceUpdatedListeners.add(listener);
|
|
612
|
+
return () => {
|
|
613
|
+
resourceUpdatedListeners.delete(listener);
|
|
614
|
+
};
|
|
615
|
+
},
|
|
616
|
+
};
|
|
98
617
|
try {
|
|
99
618
|
const serverOptions = {
|
|
100
|
-
gateway
|
|
619
|
+
gateway,
|
|
101
620
|
bridgeControl: {
|
|
102
|
-
getState:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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");
|
|
639
|
+
},
|
|
640
|
+
bootstrapSession: async () => {
|
|
641
|
+
return await runLifecycleTransition(async () => await bootstrapSessionInternal(authState));
|
|
642
|
+
},
|
|
643
|
+
attachSession: async (requestedBrowserUrl) => {
|
|
644
|
+
return await runLifecycleTransition(async () => await attachSessionInternal(requestedBrowserUrl));
|
|
645
|
+
},
|
|
646
|
+
getPresentationMode: () => refreshStatus().presentationMode,
|
|
647
|
+
setPresentationMode: async (setModeOptions) => await setPresentationMode(setModeOptions),
|
|
648
|
+
resetProfile: async () => {
|
|
649
|
+
return await runLifecycleTransition(async () => await resetProfileInternal());
|
|
650
|
+
},
|
|
109
651
|
closeBridge: async () => {
|
|
110
652
|
await closeResources();
|
|
111
653
|
},
|
|
@@ -125,7 +667,7 @@ export async function startLocalMcpBridge(options) {
|
|
|
125
667
|
await server.start();
|
|
126
668
|
}
|
|
127
669
|
catch (error) {
|
|
128
|
-
await
|
|
670
|
+
await closeResourcesInternal().catch(options.onError);
|
|
129
671
|
throw error;
|
|
130
672
|
}
|
|
131
673
|
const input = options.input ?? process.stdin;
|
|
@@ -134,18 +676,26 @@ export async function startLocalMcpBridge(options) {
|
|
|
134
676
|
options.onError?.(error);
|
|
135
677
|
});
|
|
136
678
|
};
|
|
137
|
-
const handleOwnerSessionEnded = () => {
|
|
138
|
-
void closeResources().catch((error) => {
|
|
139
|
-
options.onError?.(error);
|
|
140
|
-
});
|
|
141
|
-
};
|
|
142
679
|
input.once("end", handleInputEnded);
|
|
143
|
-
void runtime.ownerSessionEnded.then(handleOwnerSessionEnded);
|
|
144
680
|
return {
|
|
145
|
-
site
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
681
|
+
get site() {
|
|
682
|
+
return siteDefinition.id;
|
|
683
|
+
},
|
|
684
|
+
get targetUrl() {
|
|
685
|
+
return targetUrl;
|
|
686
|
+
},
|
|
687
|
+
get controlMode() {
|
|
688
|
+
return controlMode;
|
|
689
|
+
},
|
|
690
|
+
get mode() {
|
|
691
|
+
return runtimeMode;
|
|
692
|
+
},
|
|
693
|
+
get presentationMode() {
|
|
694
|
+
return presentationMode;
|
|
695
|
+
},
|
|
696
|
+
get preferredPresentationMode() {
|
|
697
|
+
return preferredPresentationMode;
|
|
698
|
+
},
|
|
149
699
|
close: async () => {
|
|
150
700
|
input.removeListener("end", handleInputEnded);
|
|
151
701
|
await closeResources();
|