Claude sessions could not work inside the user's own profile: the desktop app's browser lost synthetic pastes and typing in Medium's editor, and a browser-wide --remote-debugging-port would hand every session to any local process. The connector is a built-in extension, off until turned on in Settings > Extensions, that serves MCP on 127.0.0.1 behind a per-install token and drives one tab at a time through that tab's webContents.debugger. - Host: a "tab-control" capability (built-in add-ons only) for listing, opening, navigating, closing and focusing tabs, handing out web tabs' webContents (never Settings, extension or home tabs), and a toolbar chip plus tab outline while a client is connected; the chip's X stops it. Built-in add-ons can ship "defaultEnabled": false. - Tools: list_tabs, open_tab, navigate, activate_tab, close_tab, screenshot, read_page_text, snapshot (accessibility tree with refs), click, type (insertText + real Enter), press_key, paste_html / paste_text (system clipboard + webContents.paste, clipboard restored), upload_file (DOM.setFileInputFiles), wait_for, and evaluate behind its own switch. - Safety: first use of each site asks the user; password and one-time-code fields are refused; uploads only from folders the user added; requests from web pages or foreign Host headers are refused; every action is logged with a timestamp in the panel and actions.log. - Claude Desktop runs stdio-shim.cjs through the Theseus executable in Node mode. - Tests: unit (protocol, auth, policy, shim) and an end-to-end run on a throwaway profile that types, pastes HTML with a heading and a link, and uploads a PNG on a local page.
59 lines
2.3 KiB
JavaScript
59 lines
2.3 KiB
JavaScript
// The system clipboard, for paste_html / paste_text: put our content on it,
|
|
// and save and restore what the user had there.
|
|
//
|
|
// Electron 44 replaced the synchronous clipboard (readHTML, readImage, …)
|
|
// with the W3C-style async one (read / write of ClipboardItem). Both are
|
|
// handled; a build with the old API gets text, HTML, RTF and image back.
|
|
|
|
function makeClipboard(electron) {
|
|
const cb = electron.clipboard;
|
|
const Item = electron.ClipboardItem;
|
|
|
|
if (typeof cb.read === "function" && typeof Item === "function") {
|
|
return {
|
|
async put({ html, text }) {
|
|
const data = { "text/plain": String(text ?? "") };
|
|
if (html != null) data["text/html"] = String(html);
|
|
await cb.write([new Item(data)]);
|
|
},
|
|
// Every format the platform offers, read out now (the items' getType
|
|
// would otherwise read the clipboard again later, after our write).
|
|
async save() {
|
|
const items = await cb.read();
|
|
const out = [];
|
|
for (const it of items) {
|
|
const entry = {};
|
|
for (const type of it.types) { try { entry[type] = await it.getType(type); } catch {} }
|
|
if (Object.keys(entry).length) out.push(entry);
|
|
}
|
|
return out;
|
|
},
|
|
async restore(saved) {
|
|
if (!saved || !saved.length) { cb.clear(); return; }
|
|
try { await cb.write(saved.map((e) => new Item(e))); return; } catch {}
|
|
// A platform format that cannot be written back: keep the text at least.
|
|
for (const e of saved) {
|
|
const t = e["text/plain"];
|
|
if (t) { try { await cb.writeText(typeof t === "string" ? t : await t.text()); return; } catch {} }
|
|
}
|
|
cb.clear();
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
async put({ html, text }) { cb.write(html != null ? { html: String(html), text: String(text ?? "") } : { text: String(text ?? "") }); },
|
|
async save() {
|
|
const s = { text: cb.readText(), html: cb.readHTML(), rtf: cb.readRTF() };
|
|
try { const img = cb.readImage(); if (img && !img.isEmpty()) s.image = img; } catch {}
|
|
return s;
|
|
},
|
|
async restore(s) {
|
|
const back = {};
|
|
for (const k of ["text", "html", "rtf", "image"]) if (s && s[k]) back[k] = s[k];
|
|
if (Object.keys(back).length) cb.write(back); else cb.clear();
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = { makeClipboard };
|