168 lines
6.3 KiB
TypeScript
168 lines
6.3 KiB
TypeScript
/**
|
|
* mcp/takeover.ts — Browser control tools via direct takeover token map access
|
|
*
|
|
* Token is set implicitly per-request from the MCP API key mapping.
|
|
* No token param needed in tool calls.
|
|
*/
|
|
|
|
export interface TakeoverBridge {
|
|
sendCmd(token: string, cmd: string, args: any, timeoutMs?: number): Promise<any>;
|
|
}
|
|
|
|
let bridge: TakeoverBridge | null = null;
|
|
let activeToken: string = "";
|
|
|
|
export function setBridge(b: TakeoverBridge) { bridge = b; }
|
|
export function setActiveToken(token: string) { activeToken = token; }
|
|
|
|
function getToken(): string {
|
|
if (!activeToken) throw new Error("No takeover token — MCP key not linked");
|
|
return activeToken;
|
|
}
|
|
|
|
function effectiveToken(breakout?: string): string {
|
|
const t = getToken();
|
|
return breakout ? `${t}-breakout-${breakout}` : t;
|
|
}
|
|
|
|
async function cmd(command: string, args: any = {}, opts: { breakout?: string; timeout?: number } = {}) {
|
|
if (!bridge) throw new Error("Takeover bridge not initialized");
|
|
return bridge.sendCmd(
|
|
effectiveToken(opts.breakout),
|
|
command,
|
|
args,
|
|
opts.timeout ?? 10000,
|
|
);
|
|
}
|
|
|
|
export const tools = [
|
|
{
|
|
name: "takeover_cmd",
|
|
description: "Execute a takeover command on the browser. Commands: boxChain, getStyles, viewport, navigate, reload, resize, querySelector, click, screenshot, getValue, setValue, typeText, listBreakouts, closeBreakout, captureScreen, enableCapture, openBreakout, eval, getConsole.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
cmd: { type: "string", description: "Command name" },
|
|
args: { type: "object", description: "Command arguments" },
|
|
breakout: { type: "string", description: "Breakout name (derives token automatically)" },
|
|
timeout: { type: "number", description: "Timeout in ms (default 10000, max 60000)" },
|
|
},
|
|
required: ["cmd"],
|
|
},
|
|
},
|
|
{
|
|
name: "takeover_screenshot",
|
|
description: "Capture a WebRTC screenshot from the browser. Returns base64 JPEG image. Requires enableCapture first.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
breakout: { type: "string", description: "Breakout name" },
|
|
quality: { type: "number", description: "JPEG quality 0-1 (default 0.5)" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "takeover_health",
|
|
description: "Check browser health: viewport + capture stream status in one call.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
breakout: { type: "string", description: "Breakout name" },
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "takeover_open_breakout",
|
|
description: "Open a breakout test window. User must confirm in browser. 30s timeout.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
name: { type: "string", description: "Breakout name" },
|
|
preset: { type: "string", enum: ["mobile", "tablet", "tablet-landscape", "desktop"], description: "Size preset (default: mobile)" },
|
|
},
|
|
required: ["name"],
|
|
},
|
|
},
|
|
{
|
|
name: "takeover_enable_capture",
|
|
description: "Enable WebRTC capture on a browser window. User must pick tab. 30s timeout.",
|
|
inputSchema: {
|
|
type: "object" as const,
|
|
properties: {
|
|
breakout: { type: "string", description: "Breakout name" },
|
|
},
|
|
},
|
|
},
|
|
];
|
|
|
|
export async function handle(name: string, args: any): Promise<any> {
|
|
switch (name) {
|
|
case "takeover_cmd": {
|
|
const result = await cmd(args.cmd, args.args || {}, {
|
|
breakout: args.breakout,
|
|
timeout: args.timeout,
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
|
|
case "takeover_screenshot": {
|
|
const result = await cmd("captureScreen", { quality: args.quality ?? 0.5 }, {
|
|
breakout: args.breakout,
|
|
timeout: 15000,
|
|
});
|
|
if (result.error) {
|
|
return { content: [{ type: "text", text: `Screenshot failed: ${result.error}` }] };
|
|
}
|
|
if (result.result?.dataUrl) {
|
|
const [header, data] = result.result.dataUrl.split(",", 2);
|
|
const mimeMatch = header.match(/data:([^;]+)/);
|
|
return {
|
|
content: [{
|
|
type: "image",
|
|
data,
|
|
mimeType: mimeMatch ? mimeMatch[1] : "image/jpeg",
|
|
}],
|
|
};
|
|
}
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
|
|
case "takeover_health": {
|
|
const opts = { breakout: args.breakout, timeout: 5000 };
|
|
const [vp, cap] = await Promise.allSettled([
|
|
cmd("viewport", {}, opts),
|
|
cmd("captureScreen", { quality: 0.1 }, opts),
|
|
]);
|
|
const viewport = vp.status === "fulfilled" ? vp.value : { error: "unreachable" };
|
|
const capture = cap.status === "fulfilled"
|
|
? (cap.value.result?.dataUrl ? { active: true } : { active: false, reason: cap.value.result?.error || cap.value.error })
|
|
: { active: false, reason: "unreachable" };
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: JSON.stringify({ viewport: viewport.result || viewport, capture }, null, 2),
|
|
}],
|
|
};
|
|
}
|
|
|
|
case "takeover_open_breakout": {
|
|
const result = await cmd("openBreakout", {
|
|
name: args.name,
|
|
preset: args.preset || "mobile",
|
|
}, { timeout: 30000 });
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
|
|
case "takeover_enable_capture": {
|
|
const result = await cmd("enableCapture", {}, {
|
|
breakout: args.breakout,
|
|
timeout: 30000,
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unknown takeover tool: ${name}`);
|
|
}
|
|
}
|