hermes/backend/mcp/takeover.ts
Nico ccee249618 v0.6.42: Hermes chat UI — Vue3/TS/Vite, audio STT/TTS, sidebar rail, MCP event loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 19:35:10 +02:00

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}`);
}
}