@mario.andreschak/mcp-browser 3.46.0 → 3.46.2
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/README.md +52 -52
- package/dist/audioTap.js +120 -120
- package/dist/capture.js +40 -40
- package/dist/resources.js +290 -290
- package/dist/viewHtml.js +460 -460
- package/package.json +6 -1
- package/scripts/install-browser.mjs +212 -212
package/dist/resources.js
CHANGED
|
@@ -67,296 +67,296 @@ export function renderBrowserAppHtml(gateway) {
|
|
|
67
67
|
origin: gateway?.origin ?? '',
|
|
68
68
|
token: gateway?.token ?? '',
|
|
69
69
|
}).replace(/</g, '\\u003c');
|
|
70
|
-
return `<!doctype html>
|
|
71
|
-
<html>
|
|
72
|
-
<head>
|
|
73
|
-
<meta charset="utf-8" />
|
|
74
|
-
<style>
|
|
75
|
-
:root{--bg:#fff;--fg:#202124;--muted:#5f6368;--border:#dadce0;--danger:#c5221f}
|
|
76
|
-
html[data-theme="dark"]{--bg:#202124;--fg:#e8eaed;--muted:#9aa0a6;--border:#3c4043;--danger:#f28b82}
|
|
77
|
-
*{box-sizing:border-box}html,body{height:100%;margin:0;overflow:hidden}
|
|
78
|
-
body{background:var(--bg);color:var(--fg);font:13px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
79
|
-
#frame{width:100%;height:100%;border:0;display:block;background:var(--bg)}
|
|
80
|
-
#fallback{display:none;height:100%;flex-direction:column}
|
|
81
|
-
#fallback.show{display:flex}
|
|
82
|
-
#frame.hide{display:none}
|
|
83
|
-
.bar{display:flex;gap:6px;align-items:center;padding:6px;border-bottom:1px solid var(--border)}
|
|
84
|
-
.bar input{flex:1;min-width:0;font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit}
|
|
85
|
-
.bar button{font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit;cursor:pointer}
|
|
86
|
-
#shot{flex:1;min-height:0;display:grid;place-items:center;overflow:hidden}
|
|
87
|
-
#shot img{max-width:100%;max-height:100%;display:block}
|
|
88
|
-
#note{padding:6px 10px;color:var(--muted);font-size:12px}
|
|
89
|
-
#note.error{color:var(--danger)}
|
|
90
|
-
</style>
|
|
91
|
-
</head>
|
|
92
|
-
<body>
|
|
93
|
-
<iframe id="frame" title="Live browser view" allow="autoplay; clipboard-read; clipboard-write; fullscreen" referrerpolicy="no-referrer"></iframe>
|
|
94
|
-
<div id="fallback">
|
|
95
|
-
<div class="bar">
|
|
96
|
-
<button id="fbBack" title="Back">←</button>
|
|
97
|
-
<button id="fbForward" title="Forward">→</button>
|
|
98
|
-
<button id="fbReload" title="Reload">↻</button>
|
|
99
|
-
<input id="fbUrl" type="text" placeholder="https://example.com" />
|
|
100
|
-
<button id="fbGo">Go</button>
|
|
101
|
-
</div>
|
|
102
|
-
<div id="shot"></div>
|
|
103
|
-
<div id="note"></div>
|
|
104
|
-
</div>
|
|
105
|
-
<script>
|
|
106
|
-
(function(){
|
|
107
|
-
"use strict";
|
|
108
|
-
var GATEWAY = ${config};
|
|
109
|
-
var parentWin = window.parent, idc = 1, pending = {}, sessionId = "", sessionKey = "", starting = false, ownsSession = false;
|
|
110
|
-
var frame = document.getElementById("frame");
|
|
111
|
-
var fallback = document.getElementById("fallback");
|
|
112
|
-
var shot = document.getElementById("shot");
|
|
113
|
-
var note = document.getElementById("note");
|
|
114
|
-
var fbUrl = document.getElementById("fbUrl");
|
|
115
|
-
var theme = "light", pollTimer = 0, usingFallback = false;
|
|
116
|
-
var standaloneTimer = 0, toolResultTimer = 0, pendingToolInput = null;
|
|
117
|
-
// null = host did not report its sandbox CSP grant (unknown policy);
|
|
118
|
-
// [] = host reported a grant that excludes the gateway (denied).
|
|
119
|
-
var grantedFrameDomains = null, viewReady = false, readyTimer = 0;
|
|
120
|
-
|
|
121
|
-
function post(m){ parentWin.postMessage(m, "*"); }
|
|
122
|
-
function rpc(method, params){
|
|
123
|
-
return new Promise(function(resolve, reject){
|
|
124
|
-
var id = idc++;
|
|
125
|
-
pending[id] = { resolve: resolve, reject: reject };
|
|
126
|
-
post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
function notify(method, params){ post({ jsonrpc: "2.0", method: method, params: params || {} }); }
|
|
130
|
-
function errorText(e){ return e && e.message ? e.message : String(e); }
|
|
131
|
-
function payload(result){
|
|
132
|
-
if (result && result.structuredContent) return result.structuredContent;
|
|
133
|
-
try {
|
|
134
|
-
var t = result && result.content && result.content[0] && result.content[0].text;
|
|
135
|
-
return t ? JSON.parse(t) : {};
|
|
136
|
-
} catch (e) { return {}; }
|
|
137
|
-
}
|
|
138
|
-
function failMessage(result){
|
|
139
|
-
var p = payload(result);
|
|
140
|
-
return p && p.error && p.error.message ? p.error.message : "Browser operation failed";
|
|
141
|
-
}
|
|
142
|
-
async function call(name, args){
|
|
143
|
-
var result = await rpc("tools/call", { name: name, arguments: args || {} });
|
|
144
|
-
if (result && result.isError) throw new Error(failMessage(result));
|
|
145
|
-
return result;
|
|
146
|
-
}
|
|
147
|
-
function toView(message){
|
|
148
|
-
if (usingFallback || !frame.contentWindow) return;
|
|
149
|
-
try { frame.contentWindow.postMessage(Object.assign({ source: "flujo-browser-shell" }, message), "*"); } catch (e) {}
|
|
150
|
-
}
|
|
151
|
-
function setNote(text, isError){
|
|
152
|
-
note.textContent = text || "";
|
|
153
|
-
note.className = isError ? "error" : "";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/* ---------- live view ---------- */
|
|
157
|
-
// Whether the host's effective frame-src grant covers the gateway origin.
|
|
158
|
-
// Grants may be exact origins, the loopback port-wildcard form
|
|
159
|
-
// (http://127.0.0.1:*), or the allow-all escape hatch.
|
|
160
|
-
function frameGrantCovers(origin){
|
|
161
|
-
if (grantedFrameDomains === null) return true; /* unknown policy: probe, the ready watchdog still guards */
|
|
162
|
-
var o = String(origin || "").toLowerCase();
|
|
163
|
-
for (var i = 0; i < grantedFrameDomains.length; i++){
|
|
164
|
-
var d = String(grantedFrameDomains[i] || "").toLowerCase();
|
|
165
|
-
if (d === "*" || d === o) return true;
|
|
166
|
-
if (d.slice(-2) === ":*" && o.indexOf(d.slice(0, -1)) === 0) return true;
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
function mountLiveView(){
|
|
171
|
-
frame.src = GATEWAY.origin + "/view?s=" + encodeURIComponent(sessionId) + "&t=" + encodeURIComponent(GATEWAY.token) + "&k=" + encodeURIComponent(sessionKey);
|
|
172
|
-
// A CSP-blocked or unreachable iframe fires neither load nor error, so a
|
|
173
|
-
// silent live view must degrade to screenshots on its own.
|
|
174
|
-
clearTimeout(readyTimer);
|
|
175
|
-
readyTimer = setTimeout(function(){
|
|
176
|
-
if (!viewReady) useFallback("The live view is not reachable from this browser; showing periodic screenshots.");
|
|
177
|
-
}, 8000);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/* ---------- screenshot fallback ---------- */
|
|
181
|
-
function useFallback(reason){
|
|
182
|
-
if (usingFallback) return;
|
|
183
|
-
usingFallback = true;
|
|
184
|
-
clearTimeout(readyTimer);
|
|
185
|
-
frame.classList.add("hide");
|
|
186
|
-
fallback.classList.add("show");
|
|
187
|
-
setNote(reason || "Live streaming is unavailable; showing periodic screenshots.", true);
|
|
188
|
-
pollOnce();
|
|
189
|
-
}
|
|
190
|
-
async function pollOnce(){
|
|
191
|
-
if (!usingFallback || !sessionId) return;
|
|
192
|
-
try {
|
|
193
|
-
var result = await call("browser_screenshot", { sessionId: sessionId });
|
|
194
|
-
var items = (result && result.content) || [];
|
|
195
|
-
for (var i = 0; i < items.length; i++){
|
|
196
|
-
if (items[i].type === "image" && items[i].data){
|
|
197
|
-
shot.innerHTML = "";
|
|
198
|
-
var img = document.createElement("img");
|
|
199
|
-
img.src = "data:" + (items[i].mimeType || "image/png") + ";base64," + items[i].data;
|
|
200
|
-
img.alt = "Browser viewport";
|
|
201
|
-
shot.appendChild(img);
|
|
202
|
-
break;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
var state = payload(result);
|
|
206
|
-
if (state.url && document.activeElement !== fbUrl) fbUrl.value = state.url;
|
|
207
|
-
setNote("Screenshot fallback \\u2014 updated " + new Date().toLocaleTimeString());
|
|
208
|
-
} catch (e) {
|
|
209
|
-
setNote(errorText(e), true);
|
|
210
|
-
}
|
|
211
|
-
clearTimeout(pollTimer);
|
|
212
|
-
pollTimer = setTimeout(pollOnce, 2000);
|
|
213
|
-
}
|
|
214
|
-
function fallbackAction(kind, url){
|
|
215
|
-
return async function(){
|
|
216
|
-
try {
|
|
217
|
-
if (kind === "navigate") await call("browser_navigate", { sessionId: sessionId, url: url() });
|
|
218
|
-
else await call("browser_" + kind, { sessionId: sessionId });
|
|
219
|
-
pollOnce();
|
|
220
|
-
} catch (e) { setNote(errorText(e), true); }
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
document.getElementById("fbBack").onclick = fallbackAction("back");
|
|
224
|
-
document.getElementById("fbForward").onclick = fallbackAction("forward");
|
|
225
|
-
document.getElementById("fbReload").onclick = fallbackAction("reload");
|
|
226
|
-
document.getElementById("fbGo").onclick = fallbackAction("navigate", function(){ return fbUrl.value.trim(); });
|
|
227
|
-
fbUrl.onkeydown = function(e){ if (e.key === "Enter"){ e.preventDefault(); document.getElementById("fbGo").click(); } };
|
|
228
|
-
|
|
229
|
-
/* ---------- session ---------- */
|
|
230
|
-
async function start(initialUrl, requestedId, owned){
|
|
231
|
-
if (starting) return;
|
|
232
|
-
starting = true;
|
|
233
|
-
ownsSession = !!owned;
|
|
234
|
-
try {
|
|
235
|
-
var args = {};
|
|
236
|
-
if (initialUrl) args.url = initialUrl;
|
|
237
|
-
if (requestedId) args.sessionId = requestedId;
|
|
238
|
-
var result = await call("browser_open", args);
|
|
239
|
-
var state = payload(result);
|
|
240
|
-
sessionId = state.sessionId || "";
|
|
241
|
-
sessionKey = result && result._meta && result._meta.flujo && result._meta.flujo.gatewaySessionToken || "";
|
|
242
|
-
if (!sessionId || !sessionKey) throw new Error("The browser session handshake did not report its private capability.");
|
|
243
|
-
if (!GATEWAY.origin || !GATEWAY.token) useFallback("The live stream gateway is disabled, so this view falls back to screenshots.");
|
|
244
|
-
else if (!frameGrantCovers(GATEWAY.origin)) useFallback("This deployment cannot frame the local live-view gateway; showing periodic screenshots.");
|
|
245
|
-
else mountLiveView();
|
|
246
|
-
} catch (e) {
|
|
247
|
-
ownsSession = false;
|
|
248
|
-
starting = false;
|
|
249
|
-
useFallback(errorText(e));
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
async function teardownResource(requestId){
|
|
254
|
-
clearTimeout(pollTimer);
|
|
255
|
-
clearTimeout(readyTimer);
|
|
256
|
-
clearTimeout(standaloneTimer);
|
|
257
|
-
clearTimeout(toolResultTimer);
|
|
258
|
-
var ownedId = ownsSession ? sessionId : "";
|
|
259
|
-
ownsSession = false;
|
|
260
|
-
if (ownedId){
|
|
261
|
-
try { await call("browser_close", { sessionId: ownedId }); } catch (e) {}
|
|
262
|
-
}
|
|
263
|
-
post({ jsonrpc: "2.0", id: requestId, result: {} });
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/* ---------- commands relayed from the framed view ---------- */
|
|
267
|
-
async function runViewCommand(data){
|
|
268
|
-
try {
|
|
269
|
-
if (data.type === "navigate" && data.url) await call("browser_navigate", { sessionId: sessionId, url: data.url });
|
|
270
|
-
else if (data.type === "back") await call("browser_back", { sessionId: sessionId });
|
|
271
|
-
else if (data.type === "forward") await call("browser_forward", { sessionId: sessionId });
|
|
272
|
-
else if (data.type === "reload") await call("browser_reload", { sessionId: sessionId });
|
|
273
|
-
else if (data.type === "close"){
|
|
274
|
-
await call("browser_close", { sessionId: sessionId });
|
|
275
|
-
notify("ui/notifications/request-teardown", {});
|
|
276
|
-
} else if (data.type === "fullscreen"){
|
|
277
|
-
await rpc("ui/request-display-mode", { mode: "fullscreen" });
|
|
278
|
-
}
|
|
279
|
-
} catch (e) {
|
|
280
|
-
toView({ type: "error", message: errorText(e) });
|
|
281
|
-
} finally {
|
|
282
|
-
toView({ type: "loading", value: false });
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
window.addEventListener("message", function(event){
|
|
287
|
-
var m = event.data;
|
|
288
|
-
if (m && m.source === "flujo-browser-view"){
|
|
289
|
-
if (event.source !== frame.contentWindow) return;
|
|
290
|
-
if (m.type === "ready"){ viewReady = true; clearTimeout(readyTimer); toView({ type: "theme", theme: theme }); }
|
|
291
|
-
else void runViewCommand(m);
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (!m || m.jsonrpc !== "2.0") return;
|
|
295
|
-
if (m.id !== undefined && (m.result !== undefined || m.error !== undefined)){
|
|
296
|
-
var p = pending[m.id];
|
|
297
|
-
if (!p) return;
|
|
298
|
-
delete pending[m.id];
|
|
299
|
-
if (m.error) p.reject(new Error(m.error.message || "RPC error"));
|
|
300
|
-
else p.resolve(m.result);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (m.method === "ui/notifications/host-context-changed"){
|
|
304
|
-
theme = (m.params && m.params.theme) === "dark" ? "dark" : "light";
|
|
305
|
-
document.documentElement.setAttribute("data-theme", theme);
|
|
306
|
-
toView({ type: "theme", theme: theme });
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (m.method === "ui/notifications/tool-input"){
|
|
310
|
-
var a = (m.params && m.params.arguments) || {};
|
|
311
|
-
pendingToolInput = a;
|
|
312
|
-
clearTimeout(standaloneTimer);
|
|
313
|
-
if (!starting && typeof a.sessionId === "string" && a.sessionId){
|
|
314
|
-
void start(typeof a.url === "string" ? a.url : "", a.sessionId, false);
|
|
315
|
-
} else if (!starting){
|
|
316
|
-
// Tool results carry the authoritative sessionId. Waiting for it avoids
|
|
317
|
-
// opening a second page after a no-id browser_open created its session.
|
|
318
|
-
clearTimeout(toolResultTimer);
|
|
319
|
-
toolResultTimer = setTimeout(function(){
|
|
320
|
-
if (!starting) void start(typeof a.url === "string" ? a.url : "", "", true);
|
|
321
|
-
}, 1000);
|
|
322
|
-
}
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
if (m.method === "ui/notifications/tool-result"){
|
|
326
|
-
var toolState = payload(m.params);
|
|
327
|
-
var toolSessionId = toolState && typeof toolState.sessionId === "string" ? toolState.sessionId : "";
|
|
328
|
-
if (!starting && toolSessionId){
|
|
329
|
-
clearTimeout(toolResultTimer);
|
|
330
|
-
var input = pendingToolInput || {};
|
|
331
|
-
void start(typeof input.url === "string" ? input.url : "", toolSessionId, false);
|
|
332
|
-
}
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
if (m.method === "ping" && m.id !== undefined){ post({ jsonrpc: "2.0", id: m.id, result: {} }); return; }
|
|
336
|
-
if (m.method === "ui/resource-teardown" && m.id !== undefined){
|
|
337
|
-
void teardownResource(m.id);
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
rpc("ui/initialize", {
|
|
343
|
-
appInfo: { name: "flujo-browser-view", version: "3.0.0" },
|
|
344
|
-
appCapabilities: { availableDisplayModes: ["inline", "fullscreen", "pip"] },
|
|
345
|
-
protocolVersion: "${MCP_APPS_PROTOCOL_VERSION}"
|
|
346
|
-
}).then(function(result){
|
|
347
|
-
var sandboxCaps = result && result.capabilities && result.capabilities.sandbox;
|
|
348
|
-
if (sandboxCaps) grantedFrameDomains = (sandboxCaps.csp && sandboxCaps.csp.frameDomains) || [];
|
|
349
|
-
theme = (result && result.hostContext && result.hostContext.theme) === "dark" ? "dark" : "light";
|
|
350
|
-
document.documentElement.setAttribute("data-theme", theme);
|
|
351
|
-
notify("ui/notifications/initialized", {});
|
|
352
|
-
notify("ui/notifications/size-changed", { width: 0, height: 640 });
|
|
353
|
-
standaloneTimer = setTimeout(function(){ if (!starting) void start("", "", true); }, 300);
|
|
354
|
-
}).catch(function(e){
|
|
355
|
-
useFallback("App initialization failed: " + errorText(e));
|
|
356
|
-
});
|
|
357
|
-
})();
|
|
358
|
-
</script>
|
|
359
|
-
</body>
|
|
70
|
+
return `<!doctype html>
|
|
71
|
+
<html>
|
|
72
|
+
<head>
|
|
73
|
+
<meta charset="utf-8" />
|
|
74
|
+
<style>
|
|
75
|
+
:root{--bg:#fff;--fg:#202124;--muted:#5f6368;--border:#dadce0;--danger:#c5221f}
|
|
76
|
+
html[data-theme="dark"]{--bg:#202124;--fg:#e8eaed;--muted:#9aa0a6;--border:#3c4043;--danger:#f28b82}
|
|
77
|
+
*{box-sizing:border-box}html,body{height:100%;margin:0;overflow:hidden}
|
|
78
|
+
body{background:var(--bg);color:var(--fg);font:13px/1.45 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
79
|
+
#frame{width:100%;height:100%;border:0;display:block;background:var(--bg)}
|
|
80
|
+
#fallback{display:none;height:100%;flex-direction:column}
|
|
81
|
+
#fallback.show{display:flex}
|
|
82
|
+
#frame.hide{display:none}
|
|
83
|
+
.bar{display:flex;gap:6px;align-items:center;padding:6px;border-bottom:1px solid var(--border)}
|
|
84
|
+
.bar input{flex:1;min-width:0;font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit}
|
|
85
|
+
.bar button{font:inherit;padding:6px 10px;border:1px solid var(--border);border-radius:14px;background:var(--bg);color:inherit;cursor:pointer}
|
|
86
|
+
#shot{flex:1;min-height:0;display:grid;place-items:center;overflow:hidden}
|
|
87
|
+
#shot img{max-width:100%;max-height:100%;display:block}
|
|
88
|
+
#note{padding:6px 10px;color:var(--muted);font-size:12px}
|
|
89
|
+
#note.error{color:var(--danger)}
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<iframe id="frame" title="Live browser view" allow="autoplay; clipboard-read; clipboard-write; fullscreen" referrerpolicy="no-referrer"></iframe>
|
|
94
|
+
<div id="fallback">
|
|
95
|
+
<div class="bar">
|
|
96
|
+
<button id="fbBack" title="Back">←</button>
|
|
97
|
+
<button id="fbForward" title="Forward">→</button>
|
|
98
|
+
<button id="fbReload" title="Reload">↻</button>
|
|
99
|
+
<input id="fbUrl" type="text" placeholder="https://example.com" />
|
|
100
|
+
<button id="fbGo">Go</button>
|
|
101
|
+
</div>
|
|
102
|
+
<div id="shot"></div>
|
|
103
|
+
<div id="note"></div>
|
|
104
|
+
</div>
|
|
105
|
+
<script>
|
|
106
|
+
(function(){
|
|
107
|
+
"use strict";
|
|
108
|
+
var GATEWAY = ${config};
|
|
109
|
+
var parentWin = window.parent, idc = 1, pending = {}, sessionId = "", sessionKey = "", starting = false, ownsSession = false;
|
|
110
|
+
var frame = document.getElementById("frame");
|
|
111
|
+
var fallback = document.getElementById("fallback");
|
|
112
|
+
var shot = document.getElementById("shot");
|
|
113
|
+
var note = document.getElementById("note");
|
|
114
|
+
var fbUrl = document.getElementById("fbUrl");
|
|
115
|
+
var theme = "light", pollTimer = 0, usingFallback = false;
|
|
116
|
+
var standaloneTimer = 0, toolResultTimer = 0, pendingToolInput = null;
|
|
117
|
+
// null = host did not report its sandbox CSP grant (unknown policy);
|
|
118
|
+
// [] = host reported a grant that excludes the gateway (denied).
|
|
119
|
+
var grantedFrameDomains = null, viewReady = false, readyTimer = 0;
|
|
120
|
+
|
|
121
|
+
function post(m){ parentWin.postMessage(m, "*"); }
|
|
122
|
+
function rpc(method, params){
|
|
123
|
+
return new Promise(function(resolve, reject){
|
|
124
|
+
var id = idc++;
|
|
125
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
126
|
+
post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
function notify(method, params){ post({ jsonrpc: "2.0", method: method, params: params || {} }); }
|
|
130
|
+
function errorText(e){ return e && e.message ? e.message : String(e); }
|
|
131
|
+
function payload(result){
|
|
132
|
+
if (result && result.structuredContent) return result.structuredContent;
|
|
133
|
+
try {
|
|
134
|
+
var t = result && result.content && result.content[0] && result.content[0].text;
|
|
135
|
+
return t ? JSON.parse(t) : {};
|
|
136
|
+
} catch (e) { return {}; }
|
|
137
|
+
}
|
|
138
|
+
function failMessage(result){
|
|
139
|
+
var p = payload(result);
|
|
140
|
+
return p && p.error && p.error.message ? p.error.message : "Browser operation failed";
|
|
141
|
+
}
|
|
142
|
+
async function call(name, args){
|
|
143
|
+
var result = await rpc("tools/call", { name: name, arguments: args || {} });
|
|
144
|
+
if (result && result.isError) throw new Error(failMessage(result));
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
function toView(message){
|
|
148
|
+
if (usingFallback || !frame.contentWindow) return;
|
|
149
|
+
try { frame.contentWindow.postMessage(Object.assign({ source: "flujo-browser-shell" }, message), "*"); } catch (e) {}
|
|
150
|
+
}
|
|
151
|
+
function setNote(text, isError){
|
|
152
|
+
note.textContent = text || "";
|
|
153
|
+
note.className = isError ? "error" : "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* ---------- live view ---------- */
|
|
157
|
+
// Whether the host's effective frame-src grant covers the gateway origin.
|
|
158
|
+
// Grants may be exact origins, the loopback port-wildcard form
|
|
159
|
+
// (http://127.0.0.1:*), or the allow-all escape hatch.
|
|
160
|
+
function frameGrantCovers(origin){
|
|
161
|
+
if (grantedFrameDomains === null) return true; /* unknown policy: probe, the ready watchdog still guards */
|
|
162
|
+
var o = String(origin || "").toLowerCase();
|
|
163
|
+
for (var i = 0; i < grantedFrameDomains.length; i++){
|
|
164
|
+
var d = String(grantedFrameDomains[i] || "").toLowerCase();
|
|
165
|
+
if (d === "*" || d === o) return true;
|
|
166
|
+
if (d.slice(-2) === ":*" && o.indexOf(d.slice(0, -1)) === 0) return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
function mountLiveView(){
|
|
171
|
+
frame.src = GATEWAY.origin + "/view?s=" + encodeURIComponent(sessionId) + "&t=" + encodeURIComponent(GATEWAY.token) + "&k=" + encodeURIComponent(sessionKey);
|
|
172
|
+
// A CSP-blocked or unreachable iframe fires neither load nor error, so a
|
|
173
|
+
// silent live view must degrade to screenshots on its own.
|
|
174
|
+
clearTimeout(readyTimer);
|
|
175
|
+
readyTimer = setTimeout(function(){
|
|
176
|
+
if (!viewReady) useFallback("The live view is not reachable from this browser; showing periodic screenshots.");
|
|
177
|
+
}, 8000);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ---------- screenshot fallback ---------- */
|
|
181
|
+
function useFallback(reason){
|
|
182
|
+
if (usingFallback) return;
|
|
183
|
+
usingFallback = true;
|
|
184
|
+
clearTimeout(readyTimer);
|
|
185
|
+
frame.classList.add("hide");
|
|
186
|
+
fallback.classList.add("show");
|
|
187
|
+
setNote(reason || "Live streaming is unavailable; showing periodic screenshots.", true);
|
|
188
|
+
pollOnce();
|
|
189
|
+
}
|
|
190
|
+
async function pollOnce(){
|
|
191
|
+
if (!usingFallback || !sessionId) return;
|
|
192
|
+
try {
|
|
193
|
+
var result = await call("browser_screenshot", { sessionId: sessionId });
|
|
194
|
+
var items = (result && result.content) || [];
|
|
195
|
+
for (var i = 0; i < items.length; i++){
|
|
196
|
+
if (items[i].type === "image" && items[i].data){
|
|
197
|
+
shot.innerHTML = "";
|
|
198
|
+
var img = document.createElement("img");
|
|
199
|
+
img.src = "data:" + (items[i].mimeType || "image/png") + ";base64," + items[i].data;
|
|
200
|
+
img.alt = "Browser viewport";
|
|
201
|
+
shot.appendChild(img);
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
var state = payload(result);
|
|
206
|
+
if (state.url && document.activeElement !== fbUrl) fbUrl.value = state.url;
|
|
207
|
+
setNote("Screenshot fallback \\u2014 updated " + new Date().toLocaleTimeString());
|
|
208
|
+
} catch (e) {
|
|
209
|
+
setNote(errorText(e), true);
|
|
210
|
+
}
|
|
211
|
+
clearTimeout(pollTimer);
|
|
212
|
+
pollTimer = setTimeout(pollOnce, 2000);
|
|
213
|
+
}
|
|
214
|
+
function fallbackAction(kind, url){
|
|
215
|
+
return async function(){
|
|
216
|
+
try {
|
|
217
|
+
if (kind === "navigate") await call("browser_navigate", { sessionId: sessionId, url: url() });
|
|
218
|
+
else await call("browser_" + kind, { sessionId: sessionId });
|
|
219
|
+
pollOnce();
|
|
220
|
+
} catch (e) { setNote(errorText(e), true); }
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
document.getElementById("fbBack").onclick = fallbackAction("back");
|
|
224
|
+
document.getElementById("fbForward").onclick = fallbackAction("forward");
|
|
225
|
+
document.getElementById("fbReload").onclick = fallbackAction("reload");
|
|
226
|
+
document.getElementById("fbGo").onclick = fallbackAction("navigate", function(){ return fbUrl.value.trim(); });
|
|
227
|
+
fbUrl.onkeydown = function(e){ if (e.key === "Enter"){ e.preventDefault(); document.getElementById("fbGo").click(); } };
|
|
228
|
+
|
|
229
|
+
/* ---------- session ---------- */
|
|
230
|
+
async function start(initialUrl, requestedId, owned){
|
|
231
|
+
if (starting) return;
|
|
232
|
+
starting = true;
|
|
233
|
+
ownsSession = !!owned;
|
|
234
|
+
try {
|
|
235
|
+
var args = {};
|
|
236
|
+
if (initialUrl) args.url = initialUrl;
|
|
237
|
+
if (requestedId) args.sessionId = requestedId;
|
|
238
|
+
var result = await call("browser_open", args);
|
|
239
|
+
var state = payload(result);
|
|
240
|
+
sessionId = state.sessionId || "";
|
|
241
|
+
sessionKey = result && result._meta && result._meta.flujo && result._meta.flujo.gatewaySessionToken || "";
|
|
242
|
+
if (!sessionId || !sessionKey) throw new Error("The browser session handshake did not report its private capability.");
|
|
243
|
+
if (!GATEWAY.origin || !GATEWAY.token) useFallback("The live stream gateway is disabled, so this view falls back to screenshots.");
|
|
244
|
+
else if (!frameGrantCovers(GATEWAY.origin)) useFallback("This deployment cannot frame the local live-view gateway; showing periodic screenshots.");
|
|
245
|
+
else mountLiveView();
|
|
246
|
+
} catch (e) {
|
|
247
|
+
ownsSession = false;
|
|
248
|
+
starting = false;
|
|
249
|
+
useFallback(errorText(e));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function teardownResource(requestId){
|
|
254
|
+
clearTimeout(pollTimer);
|
|
255
|
+
clearTimeout(readyTimer);
|
|
256
|
+
clearTimeout(standaloneTimer);
|
|
257
|
+
clearTimeout(toolResultTimer);
|
|
258
|
+
var ownedId = ownsSession ? sessionId : "";
|
|
259
|
+
ownsSession = false;
|
|
260
|
+
if (ownedId){
|
|
261
|
+
try { await call("browser_close", { sessionId: ownedId }); } catch (e) {}
|
|
262
|
+
}
|
|
263
|
+
post({ jsonrpc: "2.0", id: requestId, result: {} });
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ---------- commands relayed from the framed view ---------- */
|
|
267
|
+
async function runViewCommand(data){
|
|
268
|
+
try {
|
|
269
|
+
if (data.type === "navigate" && data.url) await call("browser_navigate", { sessionId: sessionId, url: data.url });
|
|
270
|
+
else if (data.type === "back") await call("browser_back", { sessionId: sessionId });
|
|
271
|
+
else if (data.type === "forward") await call("browser_forward", { sessionId: sessionId });
|
|
272
|
+
else if (data.type === "reload") await call("browser_reload", { sessionId: sessionId });
|
|
273
|
+
else if (data.type === "close"){
|
|
274
|
+
await call("browser_close", { sessionId: sessionId });
|
|
275
|
+
notify("ui/notifications/request-teardown", {});
|
|
276
|
+
} else if (data.type === "fullscreen"){
|
|
277
|
+
await rpc("ui/request-display-mode", { mode: "fullscreen" });
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
toView({ type: "error", message: errorText(e) });
|
|
281
|
+
} finally {
|
|
282
|
+
toView({ type: "loading", value: false });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
window.addEventListener("message", function(event){
|
|
287
|
+
var m = event.data;
|
|
288
|
+
if (m && m.source === "flujo-browser-view"){
|
|
289
|
+
if (event.source !== frame.contentWindow) return;
|
|
290
|
+
if (m.type === "ready"){ viewReady = true; clearTimeout(readyTimer); toView({ type: "theme", theme: theme }); }
|
|
291
|
+
else void runViewCommand(m);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (!m || m.jsonrpc !== "2.0") return;
|
|
295
|
+
if (m.id !== undefined && (m.result !== undefined || m.error !== undefined)){
|
|
296
|
+
var p = pending[m.id];
|
|
297
|
+
if (!p) return;
|
|
298
|
+
delete pending[m.id];
|
|
299
|
+
if (m.error) p.reject(new Error(m.error.message || "RPC error"));
|
|
300
|
+
else p.resolve(m.result);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (m.method === "ui/notifications/host-context-changed"){
|
|
304
|
+
theme = (m.params && m.params.theme) === "dark" ? "dark" : "light";
|
|
305
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
306
|
+
toView({ type: "theme", theme: theme });
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (m.method === "ui/notifications/tool-input"){
|
|
310
|
+
var a = (m.params && m.params.arguments) || {};
|
|
311
|
+
pendingToolInput = a;
|
|
312
|
+
clearTimeout(standaloneTimer);
|
|
313
|
+
if (!starting && typeof a.sessionId === "string" && a.sessionId){
|
|
314
|
+
void start(typeof a.url === "string" ? a.url : "", a.sessionId, false);
|
|
315
|
+
} else if (!starting){
|
|
316
|
+
// Tool results carry the authoritative sessionId. Waiting for it avoids
|
|
317
|
+
// opening a second page after a no-id browser_open created its session.
|
|
318
|
+
clearTimeout(toolResultTimer);
|
|
319
|
+
toolResultTimer = setTimeout(function(){
|
|
320
|
+
if (!starting) void start(typeof a.url === "string" ? a.url : "", "", true);
|
|
321
|
+
}, 1000);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (m.method === "ui/notifications/tool-result"){
|
|
326
|
+
var toolState = payload(m.params);
|
|
327
|
+
var toolSessionId = toolState && typeof toolState.sessionId === "string" ? toolState.sessionId : "";
|
|
328
|
+
if (!starting && toolSessionId){
|
|
329
|
+
clearTimeout(toolResultTimer);
|
|
330
|
+
var input = pendingToolInput || {};
|
|
331
|
+
void start(typeof input.url === "string" ? input.url : "", toolSessionId, false);
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (m.method === "ping" && m.id !== undefined){ post({ jsonrpc: "2.0", id: m.id, result: {} }); return; }
|
|
336
|
+
if (m.method === "ui/resource-teardown" && m.id !== undefined){
|
|
337
|
+
void teardownResource(m.id);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
rpc("ui/initialize", {
|
|
343
|
+
appInfo: { name: "flujo-browser-view", version: "3.0.0" },
|
|
344
|
+
appCapabilities: { availableDisplayModes: ["inline", "fullscreen", "pip"] },
|
|
345
|
+
protocolVersion: "${MCP_APPS_PROTOCOL_VERSION}"
|
|
346
|
+
}).then(function(result){
|
|
347
|
+
var sandboxCaps = result && result.capabilities && result.capabilities.sandbox;
|
|
348
|
+
if (sandboxCaps) grantedFrameDomains = (sandboxCaps.csp && sandboxCaps.csp.frameDomains) || [];
|
|
349
|
+
theme = (result && result.hostContext && result.hostContext.theme) === "dark" ? "dark" : "light";
|
|
350
|
+
document.documentElement.setAttribute("data-theme", theme);
|
|
351
|
+
notify("ui/notifications/initialized", {});
|
|
352
|
+
notify("ui/notifications/size-changed", { width: 0, height: 640 });
|
|
353
|
+
standaloneTimer = setTimeout(function(){ if (!starting) void start("", "", true); }, 300);
|
|
354
|
+
}).catch(function(e){
|
|
355
|
+
useFallback("App initialization failed: " + errorText(e));
|
|
356
|
+
});
|
|
357
|
+
})();
|
|
358
|
+
</script>
|
|
359
|
+
</body>
|
|
360
360
|
</html>`;
|
|
361
361
|
}
|
|
362
362
|
//# sourceMappingURL=resources.js.map
|