Personae

io.github.leek-emperor由 GitHub 账号 leek-emperor 发布v0.2.1更新于 Oct 24, 2026

Control specific, already-open account-isolated Personae browser identities with MCP.

已验证STDIO仅桌面浏览器自动化安全与监控

概览

AI 生成的概览

让助手驱动已打开的 Personae 浏览器身份窗口,并保持各账号的 Cookie 与登录状态相互隔离。

可以做什么
Personae 是一款 Electron 桌面浏览器,每个身份都是独立的 Chromium 分区,其 MCP 服务器把这些窗口开放给智能体。工具包括 list_identities、open_identity、snapshot、navigate、click、fill、press、act、get_text、get_url、screenshot、eval_js 和 load_skill。每个工具都接受 identity 参数,可选参数 target 用于操作弹出式子窗口。该服务器只是一个 stdio 启动器,通过本地桥接连接到正在运行的应用。
适用场景
适用于浏览器窗口已经存在、且分属不同账号的场景,助手需要附着到某个指定身份而不越界到其他身份。也适合同一网站在多个登录下同时打开、助手必须操作正确窗口的多账号工作流。
运行要求
需要单独安装并正在运行的 Personae 桌面应用;npm 包只是启动器。该应用仅支持桌面端,已在 macOS arm64 上验证;Windows 有打包产物但运行时未经验证,Linux 不是构建目标。未声明 API 密钥,也未声明环境变量。推荐方式使用应用自带的运行时,因此无需安装 Node。
安装前请注意
CDP 端口没有访问控制:任何本地进程只要连接就能驱动全部身份、跨越分区边界,因此不要在共享机器上处理敏感账号。工具可以导航、点击、填写、按键并执行 JavaScript,会改变页面状态并可能提交数据。代理密码通过 safeStorage 存储,不可用时回退为明文。它不是反检测浏览器,不做指纹伪装。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Personae,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。

其他 MCP 客户端

参照 仓库 中的启动说明。

README

[Personae]

Personae

A multi-identity browser · every identity is an isolated partition, every window is agent-controllable

Bundles agent-browser and an MCP server — no Node or CLI install needed

English · 中文

An Electron desktop browser that combines two things: multi-account isolation and letting AI agents drive the browser.

  • Each browser identity is its own Chromium persist: partition — cookies, localStorage and login state are fully isolated, so you can be signed into the same site under several accounts at once.
  • Each identity is a standalone window with back/forward buttons and an address bar. It behaves like an ordinary browser.
  • The agent-browser binary is bundled, and those windows are exposed over MCP, so Codex / Claude Code can drive the page of a specific identity.

Users don't need to pre-install Node, Chrome for Testing, or any CLI.

MCP Registry

Personae is published to the official MCP Registry as io.github.leek-emperor/personae after its first Registry-enabled release. The Registry package is a small stdio launcher for a running, separately installed Personae desktop app — it does not replace the browser application or create a cloud browser.

After installing and launching Personae, a client that supports npm MCP packages can run:

bash
npx -y @leek-emperor/personae-mcp

The launcher discovers Personae through its local bridge and keeps each tool call scoped to the identity named in the request. It uses no API key and opens no public network listener. The app's Configure Codex control remains the recommended setup path because it uses the bundled Electron runtime and needs no Node installation.

The problem it solves

Existing browser-automation tools assume the agent launches the browser. If your product is a browser client, it's the other way around: the windows already exist, and they belong to different account identities. What the agent needs is to attach without crossing identities.

The approach here:

  1. The app opens a CDP port on startup (system-assigned, loopback only).
  2. When an identity window opens, the main process resolves the authoritative targetId of its content page via Target.getTargetInfo.
  3. A local bridge exposes the identity → targetId mapping.
  4. The MCP server uses that targetId directly as a tab ref — a single-step hit.

So snapshot(identity: "Account A") and snapshot(identity: "Account B") always land on the right window, even when both have the same site open with identical titles and URLs.

Quick start

Requires Node ≥ 20 and pnpm (for development only; the packaged app depends on neither).

agent-browser must be ≥ 0.36. Below that, tab list --json does not emit targetId, which is the only thing tying an identity to its window — every tool would fail to find its target. The version in package.json is already pinned; don't downgrade it.

bash
pnpm installpnpm bundle:ab      # copy the agent-browser binary and core skill into resources/pnpm dev

Add one or two browser identities in the UI, then click an identity to open its window.

The interface is in English by default. Use the EN / 中 toggle in the top-right corner to switch to Chinese; the choice is remembered and also applies to the toolbar inside each identity window and to the agent prompt you copy out.

Tests

The tricky, mostly-undocumented decisions live in small pure modules so they can be locked down by tests. They run on node:test with no extra dependencies:

bash
pnpm test

Covered: the window.open / popup decision (window-open.ts), the proxy input parsing (proxy.ts), address-bar URL normalization (url-input.ts), the Codex-config TOML splice (toml.ts), and the identity palette (colors.ts).

Packaging:

bash
pnpm build:mac      # or build:win

The default is adhoc signing (identity: '-'), which needs no Apple Developer certificate and runs fine on the build machine. But an adhoc-signed app cannot be distributed — Gatekeeper will block it on someone else's Mac. For real distribution, supply a certificate via environment variables and set notarize to true:

bash
CSC_LINK=/path/to/cert.p12 CSC_KEY_PASSWORD=... \APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=... \pnpm build:mac

Connecting Codex / Claude Code

The fastest path: let the agent connect itself

The UI has a "Let the agent connect itself" block that renders a ready-to-send prompt. Copy it, paste it into Codex or Claude Code, and the agent will write its own MCP config, restart, and verify the link by calling list_identities.

The prompt isn't just a config snippet — it also tells the agent what the 13 tools do and which mistakes to avoid (stale refs, guessing at agent-browser syntax, trying to tell identities apart by page title). It's generated from live runtime values, so the paths in it are always correct for the machine it's running on.

Or click one button

The same panel has a one-click install that writes the MCP server into ~/.codex/config.toml (idempotent, with an automatic backup first). Paths are resolved at runtime from process.execPath, so they can't be wrong.

Or configure it by hand

On macOS:

toml
[mcp_servers.personae]command = "/Applications/Personae.app/Contents/MacOS/Personae"args = ["/Applications/Personae.app/Contents/Resources/mcp-server.mjs"]
[mcp_servers.personae.env]ELECTRON_RUN_AS_NODE = "1"

The executable under Contents/MacOS/ follows productName, so it's Personae. Note that electron-builder's executableName only applies to Windows.

command points at the app's own binary; ELECTRON_RUN_AS_NODE=1 makes it degrade into a plain Node runtime. That's why no Node install is required (verified: with PATH set to /usr/bin:/bin, the full flow still works).

Claude Code:

bash
claude mcp add personae \  --env ELECTRON_RUN_AS_NODE=1 \  -- /Applications/Personae.app/Contents/MacOS/Personae \     /Applications/Personae.app/Contents/Resources/mcp-server.mjs

MCP tools

Every tool takes an identity argument (name or id).

ToolPurpose
load_skillFetch the real command syntax of the bundled agent-browser version (returns a section index by default; pass section for a specific one)
list_identitiesList all identities with open state and current URL
open_identityOpen an identity's window
snapshotAccessibility-tree snapshot returning [ref=eN] element refs
navigateNavigate to a URL
click / fill / pressInteraction; click accepts either a ref or visible text
actRun several commands inside one agent-browser process
get_text / get_urlRead content
screenshotCapture a screenshot
eval_jsEvaluate JavaScript

When click / fill receive an @eN ref they automatically take a snapshot first, because refs are only valid within a single agent-browser process — reusing one across processes always fails with Unknown ref. For multi-step interactions, put them in one act batch.

Driving a popup child window. When a page opens a popup (an OAuth sign-in, a share dialog…), that popup becomes a child window belonging to the same identity (see Popups and OAuth). It's a separate CDP target, listed under the identity in list_identities as children. Every acting/reading tool takes an optional target argument — pass a child's targetId (or its index) to drive the popup instead of the main window; omit it to drive the main window as before.

Architecture

┌─────────────────────────────────────────────────────────┐│  Electron main process                                   ││                                                          ││  ├─ CDP server (port 0 → system-assigned, 127.0.0.1 only)││  ├─ IdentityManager   storage / window lifecycle / target ││  └─ Agent Bridge      local HTTP, exposes identity↔target │└───────────┬─────────────────────────────────┬────────────┘            │                                 │  ┌─────────▼──────────┐          ┌───────────▼───────────┐  │ Identity window    │          │  Discovery file       │  │ (one per identity) │          │  userData/            │  │                    │          │  agent-bridge.json    │  │ BrowserWindow shell│          │  (port changes every  │  │  ├ WebContentsView │          │   launch; found via   │  │  │   chrome.html   │          │   a fixed path)       │  │  └ WebContentsView │          └───────────┬───────────┘  │      content       │                      │  │      ↑ agent acts  │                      │  └────────────────────┘                      │                                              │            ┌─────────────────────────────────▼───────────┐            │  scripts/mcp-server.mjs (stdio JSON-RPC)    │            │  read discovery → get targetId → run CLI    │            └───────────────────┬─────────────────────────┘                                │                    ┌───────────▼────────────┐                    │  Codex / Claude Code   │                    └────────────────────────┘

Why each identity window is "a shell plus two WebContentsViews"

The goal is to wrap third-party pages in our own navigation UI without using the <webview> tag. So:

  • the shell BrowserWindow is only a container and loads about:blank;
  • the top bar is a local chrome.html with a dedicated preload exposing just six navigation methods;
  • the content area is a WebContentsView where the partition applies, and it deliberately loads no preload at all, keeping third-party pages untouched.

Nested BrowserWindows and BrowserView both work too, and the agent can drive all three (all verified). WebContentsView was chosen because BrowserView is marked @deprecated in Electron's type definitions, and because a nested window is a separate native window on macOS — dragging, resizing and minimizing would all need manual bounds syncing, which never quite feels like "one browser window".

Popups and OAuth

A page can open a popup with window.open(url, name, features) — OAuth sign-ins, "share to…" dialogs, and so on. These need to be real windows: an OAuth popup relies on window.opener and postMessage/window.close() to hand the result back, so it can't be flattened into an in-page navigation without breaking the flow.

The approach here treats a popup as a first-class child of its identity, without ever trying to guess "is this an OAuth window?":

  • Same-window vs child. A plain <a target="_blank"> or a feature-less window.open stays an in-window navigation (no opener, no new target) — that's safe and keeps the "one identity, one main target" invariant. Only a new-window-disposition open (i.e. window.open with window features) becomes a real child window.
  • Belongs to the identity. The child is created with parent set to that identity's shell window and the same partition, so it floats over the right window and its cookies land in the right account. window.opener is preserved, so every OAuth style works. outlivesOpener: false means closing the identity closes its popups.
  • Popup blocker (on by default). The only thing distinguishing "should open" from "shouldn't" is user activation — was there a real click/keypress in the last second? This is exactly how mainstream browsers' popup blockers work: it never inspects the domain or content. Script-driven pop-unders (no preceding input) are blocked and surfaced as a quiet toast; you can turn the blocker off in the top bar.
  • Agents can drive it. Each child is a separate CDP target, resolved to its authoritative targetId and listed under the identity as children. Every MCP tool takes an optional target to address a specific popup. The "one main target per identity" contract for the outside world isn't broken — it's honestly extended to "one main plus tracked children".

The decision itself (same-window / allow-child / ignore) lives in a pure function, src/main/window-open.ts, and is unit-tested.

Proxy per identity

Each identity can route through its own proxy — useful when you want the same site's accounts to come from different regions, or to test geo-behaviour. You bring your own proxy (buy it from IPRoyal / Webshare / Bright Data / whoever); Personae only provides the setting and wires it up.

  • Where it applies. A proxy is set on the identity's persist: partition via session.setProxy, so it's genuinely per-identity — identity A can exit via a US IP while identity B exits via Japan. No proxy set means direct connection.
  • Supported. HTTP, HTTPS and SOCKS5. You can fill the fields separately, or paste a full string like socks5://user:pass@host:port into the host box.
  • Authentication. Proxies with a username/password work. Credentials never go into the proxy rules string (Chromium rejects user:pass@ there); auth is answered through Electron's login event instead.
  • Passwords are encrypted at rest. The password is stored via safeStorage (Keychain on macOS, DPAPI on Windows) in userData/proxy-secrets.json, never in identities.json and never sent back to the renderer. Where safeStorage is unavailable it falls back to plaintext and the UI says so.
  • Test connection. A button fetches an IP-echo service through that identity's session and shows the actual egress IP, so you can confirm the proxy is really in effect.
  • WebRTC leak guard. For any identity with a proxy, setWebRTCIPHandlingPolicy('disable_non_proxied_udp') is applied so WebRTC can't bypass the proxy and leak your real IP.

This is not an anti-detect browser. It changes your network exit, nothing more. Beyond the WebRTC guard above, it does not do fingerprint spoofing (Canvas / WebGL / UA / timezone / fonts…), so several identities on the same machine still share the same browser fingerprint. If a platform correlates accounts by fingerprint, a proxy alone won't hide that.

The proxy parsing (parseProxyInput / buildProxyRules) is a pure function in src/main/proxy.ts and is unit-tested, including that credentials never leak into the rules string.

Things that bit us

Kept here because most of them aren't documented anywhere.

The CDP port is per browser process. --remote-debugging-port is a process-level switch and has no webPreferences counterpart, so one Electron app has exactly one CDP server; you cannot give each window its own port. Use port 0 to let the system assign one, then read it back from userData/DevToolsActivePort — that file is written before the bind succeeds, so also probe /json/version for liveness.

--pin-tab simply fails on Electron. It triggers Target.createTarget, which Electron reports as Not supported. Worse, pinning is sticky: once a session has used it, the state is persisted and even close --all won't clear it, breaking every later command. So pass --no-pin-tab explicitly on every call.

A page target with an empty title hangs agent-browser. If the shell window never calls loadURL, it appears in CDP as a page target with empty url and title; attaching to it hangs forever (tab list returns nothing, not even a timeout). Hence the shell must load about:blank and set a readable title.

Use about:blank rather than data:text/html,<title>... — non-ASCII titles in the latter get decoded as latin-1 and turn into mojibake.

Setting the title also has a race: about:blank loads so fast that did-finish-load may fire before the listener is attached, leaving the title stuck at about:blank when several identities open concurrently. Fix: call setTitle synchronously once, then again after load.

Never identify a window by title, url, or tab index. Titles and URLs are identical when several identities open the same site, and the CDP target order does not match agent-browser's tab index order, so indices can't be derived either. Only targetId works.

agent-browser's skill content changes between versions — don't copy syntax off the web. Its own SKILL.md states explicitly that it contains no command syntax and requires skills get to fetch it from the CLI. This project proved the point: tab --url "*settings*" and --pin-tab found online don't exist in earlier versions, while tab list --json emitting targetId is a newer capability — and this project's entire targeting scheme is built on it.

agent-browser searches upward from the binary's location for a skills/ directory, which can collide with an unrelated project's directory of the same name. Pass AGENT_BROWSER_SKILLS_DIR explicitly.

A bundled binary can't rely on asarUnpack alone. asarUnpack places files under app.asar.unpacked/resources/bin/, while process.resourcesPath points at Contents/Resources — not the same location, so the binary isn't found after packaging. Use extraResources to align the paths, and keep asarUnpack limited to files actually imported with ?asset, otherwise the same binary ships twice.

On macOS you can't just "skip signing". Setting identity: null makes electron-builder skip signing entirely; the bundle keeps Electron's original adhoc signature, but the resources have been modified, so signature and content disagree and the app refuses to launch silently (no error, no log; spctl reports code has no resources but signature indicates they must be present). The correct approach is identity: '-' for adhoc signing plus com.apple.security.cs.disable-library-validation in the entitlements — set both entitlements and entitlementsInherit, since the former covers the main process.

The userData directory follows package.json's name, while the executable follows productName. These are two different fields, and executableName only applies to Windows — so if they disagree, you will look in the wrong place. This project deliberately sets both to Personae to avoid that. (Watch out on Linux: its filesystem is case-sensitive, so a directory created under an older lowercase name won't be found.)

publish: generic with a placeholder URL crashes packaging at the very last step. electron-builder's template ships provider: generic with url: https://example.com/auto-updates. Switching to provider: github makes it try to infer a release channel at the end of packaging, and without owner/repo context it throws TypeError: Cannot read properties of null (reading 'channel') — the build is already complete, yet it exits as a failure. This project publishes via gh release upload in CI, so publish: null it is.

A job-level if in GitHub Actions cannot access the matrix context. Writing if: inputs.platforms == matrix.name to filter platforms fails silently (actionlint flags context "matrix" is not allowed here, but GitHub itself doesn't complain), so every platform gets built regardless of the input. Generate the matrix JSON in an upstream job and feed it to strategy.matrix with fromJSON.

When rendering SVG with Electron, suppress window-all-closed. The icon script loops "create window → capture → destroy → create next", and each destroy() drops the window count to zero, which by default quits the whole app — the symptom is a crash after only the first size, with the child reporting No rendezvous client, terminating process (parent died?). It looks like a timing issue but retrying doesn't help. Register an empty window-all-closed handler.

Two more: on Retina displays capturePage outputs at devicePixelRatio (asking for 512 yields 1024), so pin zoomFactor / deviceScaleFactor; and inlining a moderately long SVG into data:text/html;base64,... exceeds the URL length limit and makes loadURL fail with ERR_FAILED (-2), so use a temp file with a file:// reference.

Known limitations and security notes

  • Isolation is a convention, not an architectural boundary. The CDP port has no access control. It only listens on loopback with a random port, but any local process that connects can drive every identity, crossing partition boundaries. That's the direct cost of letting external agents attach. Don't handle sensitive accounts on a shared machine.
  • In-window navigation for ordinary links. setWindowOpenHandler turns window.open and target="_blank" links into same-window navigation so that one identity always maps to one main target. Genuine popups (window.open with features, e.g. OAuth) are the exception — they open as tracked child windows; see Popups and OAuth.
  • Each identity occupies at least 3 CDP page targets (shell + top bar + content), plus one more per open popup child, so it scales at roughly 3× the identity count.
  • The bundled agent-browser version is pinned at build time; upstream fixes are not picked up automatically.
  • Runtime behaviour is only verified on macOS (arm64), including the packaged build. Windows packaging succeeds in CI (macOS + Windows are both built and produce installers), but the app has never actually been run on Windows, so runtime behaviour there is unverified. Linux is not a build target. bundle:ab bundles for the current platform only.
  • Adhoc-signed builds are not distributable: they run only on the build machine and are blocked by Gatekeeper elsewhere. Real distribution needs your own certificate and notarization.
  • The Codex side has not been verified with a real client: the MCP flow was tested with a script acting as the client (including the packaged build in a Node-free environment), but never against an actual codex run.
  • Proxy support only changes the network exit, not the fingerprint. See Proxy per identity — it is not an anti-detect browser.

Project layout

src/main/  cdp.ts          CDP port setup/discovery, targetId resolution  identity.ts     identity storage, window lifecycle, nav-bar IPC, proxy  window-open.ts  pure decision for window.open / target=_blank (unit-tested)  proxy.ts        proxy input parsing → proxyRules (unit-tested)  secret-store.ts safeStorage-backed proxy password storage  url-input.ts    address-bar input → URL normalization (unit-tested)  toml.ts         TOML section splice for the Codex config (unit-tested)  agent-bridge.ts local HTTP bridge + discovery file  mcp-setup.ts    one-click Codex configuration  index.ts        main entry and IPC registrationsrc/shared/  colors.ts       identity palette — imported by BOTH main and renderer,                  so a window's top-bar dot always matches its list entrysrc/preload/  index.ts        main-window API  chrome.ts       top-bar preload (navigation methods only)src/renderer/  chrome.html     navigation-bar UI  src/App.tsx     identity management and connection panel  src/ProxyPanel.tsx     per-identity proxy settings UI  src/agent-prompt.ts    builds the copy-and-paste prompt for agents  src/assets/fonts/      self-hosted latin subsets (the CSP blocks                         external font CDNs; CJK falls back to the system)scripts/  mcp-server.mjs           MCP server (stdio JSON-RPC)  bundle-agent-browser.mjs bundles the binary and core skill  make-icons.mjs           SVG → PNG / icns / icotest/  *.test.ts                node:test unit tests for the pure modules abovedesign/logo/  icon.svg                 icon source (edit this, then run pnpm icons)  concept*.svg             alternative concepts from the design pass.github/workflows/  release.yml     build and publish a GitHub Release (manual)  check.yml       lint / typecheck / packaging smoke test (manual)

resources/bin/ and resources/skills/ are generated by pnpm bundle:ab and not committed.

Icon

The source is design/logo/icon.svg — three non-overlapping coloured cards for three isolated identities, with a pointer for agent control. After editing the SVG, regenerate every platform format:

bash
pnpm icons

This produces build/icon.png (1024), build/icon.icns, build/icon.ico and resources/icon.png (512, the runtime window icon).

Rendering goes through Electron's bundled Chromium, so rsvg-convert / Inkscape / ImageMagick aren't needed — those are usually absent from a clean environment, whereas Electron is a dependency this project has anyway. .icns is assembled by the system iconutil; .ico is written byte by byte.

Releasing

Both workflows are manual only (workflow_dispatch) and never run on push or tag.

Go to Actions on GitHub, pick a workflow, then Run workflow:

WorkflowPurposeInputs
Build & ReleaseBuild macOS + Windows, create a Releaseversion (optional), platform (all / macos / windows), create release, prerelease
Checklint + typecheck + packaging smoke testwhether to run packaging

The Release is created as a draft; review the assets, then hit Publish yourself. If the tag already exists, assets are appended to it (--clobber overwrites same-named files), so re-runs don't just fail.

Publishing does not use electron-builder's own publish step (publish: null in electron-builder.yml); a single aggregation job uploads everything with gh release upload instead, because three platforms running in parallel would otherwise each try to create the same Release and clobber one another.

When the Build & Release workflow creates a release, it also publishes the version-matched @leek-emperor/personae-mcp stdio launcher to npm and then publishes server.json with the official mcp-publisher CLI. Set the repository NPM_TOKEN secret (an npm automation token with permission to publish that public package) before the first release; MCP Registry authentication uses GitHub OIDC and needs no separate secret.

macOS artifacts from CI are adhoc-signed too. For real signing, add CSC_LINK / CSC_KEY_PASSWORD to the repository secrets and set notarize to true in electron-builder.yml.

License

MIT — see LICENSE.

The bundled agent-browser is Apache-2.0; copyright belongs to its authors.

来源:README.md,提交 286f36b

工具

0
SourceWeft 尚未在沙箱中启动过该软件包,因此其工具未知。

版本历史

1
  1. v0.2.1最新Sep 16, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分