Personae MCP

Personae permite que los agentes de IA usen MCP para controlar de forma segura una identidad de navegador específica ya abierta y aislada por cuenta, sin mezclar sesiones ni lanzar un navegador separado.

Documentación

Personae

Personae

Un navegador multi-identidad · cada identidad es una partición aislada, cada ventana es controlable por agentes

Incluye navegador de agente y un servidor MCP — no se necesita Node ni instalación de CLI

English · 中文

Un navegador de escritorio basado en Electron que combina dos cosas: aislamiento multi-cuenta y permitir que agentes de IA controlen el navegador.

  • Cada identidad de navegador es su propia partición de Chromium persist: — las cookies, localStorage y el estado de inicio de sesión están completamente aislados, por lo que puedes estar conectado al mismo sitio con varias cuentas a la vez.
  • Cada identidad es una ventana independiente con botones de atrás/adelante y una barra de direcciones. Se comporta como un navegador normal.
  • El binario agent-browser está incluido, y esas ventanas se exponen a través de MCP, para que Codex / Claude Code puedan controlar la página de una identidad específica.

Los usuarios no necesitan preinstalar Node, Chrome for Testing, ni ningún CLI.

Registro MCP

Personae está publicado en el Registro MCP oficial como io.github.leek-emperor/personae después de su primera versión habilitada para el Registro. El paquete del Registro es un pequeño lanzador stdio para una aplicación de escritorio Personae instalada por separado y en ejecución — no reemplaza la aplicación del navegador ni crea un navegador en la nube.

Después de instalar y lanzar Personae, un cliente que admita paquetes MCP npm puede ejecutar:

npx -y @leek-emperor/personae-mcp

El lanzador descubre Personae a través de su puente local y mantiene cada llamada de herramienta limitada a la identidad nombrada en la solicitud. No usa ninguna clave API y no abre ningún listener de red público. El control Configure Codex de la aplicación sigue siendo la ruta de configuración recomendada porque usa el runtime de Electron incluido y no requiere instalación de Node.

El problema que resuelve

Las herramientas existentes de automatización de navegador asumen que el agente lanza el navegador. Si tu producto es un cliente de navegador, es al revés: las ventanas ya existen y pertenecen a diferentes identidades de cuenta. Lo que el agente necesita es conectarse sin cruzar identidades.

El enfoque aquí:

  1. La aplicación abre un puerto CDP al inicio (asignado por el sistema, solo loopback).
  2. Cuando se abre una ventana de identidad, el proceso principal resuelve el targetId autoritativo de su página de contenido mediante Target.getTargetInfo.
  3. Un puente local expone el mapeo identity → targetId.
  4. El servidor MCP usa ese targetId directamente como referencia de pestaña — un golpe de un solo paso.

Así que snapshot(identity: "Account A") y snapshot(identity: "Account B") siempre aterrizan en la ventana correcta, incluso cuando ambas tienen el mismo sitio abierto con títulos y URLs idénticos.

Inicio rápido

Requiere Node ≥ 20 y pnpm (solo para desarrollo; la aplicación empaquetada no depende de ninguno).

agent-browser debe ser ≥ 0.36. Por debajo de eso, tab list --json no emite targetId, que es lo único que vincula una identidad a su ventana — cada herramienta fallaría al encontrar su objetivo. La versión en package.json ya está fijada; no la degraden.

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

Agrega una o dos identidades de navegador en la interfaz, luego haz clic en una identidad para abrir su ventana.

La interfaz está en inglés por defecto. Usa el interruptor EN / 中 en la esquina superior derecha para cambiar al chino; la elección se recuerda y también se aplica a la barra de herramientas dentro de cada ventana de identidad y al prompt del agente que copias.

Pruebas

Las decisiones complicadas y mayormente no documentadas viven en módulos puros pequeños para que puedan ser aseguradas por pruebas. Se ejecutan en node:test sin dependencias adicionales:

pnpm test

Cubierto: la decisión de window.open / popup (window-open.ts), el análisis de entrada del proxy (proxy.ts), la normalización de URL de la barra de direcciones (url-input.ts), el empalme TOML de configuración de Codex (toml.ts) y la paleta de identidades (colors.ts).

Empaquetado:

pnpm build:mac      # or build:win

El valor predeterminado es firma adhoc (identity: '-'), que no necesita certificado de Apple Developer y funciona bien en la máquina de compilación. Pero una aplicación con firma adhoc no se puede distribuir — Gatekeeper la bloqueará en la Mac de otra persona. Para distribución real, proporciona un certificado mediante variables de entorno y establece notarize a true:

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

Conectar Codex / Claude Code

La ruta más rápida: deja que el agente se conecte solo

La interfaz tiene un bloque "Let the agent connect itself" que muestra un prompt listo para enviar. Cópialo, pégalo en Codex o Claude Code, y el agente escribirá su propia configuración MCP, reiniciará y verificará el enlace llamando a list_identities.

El prompt no es solo un fragmento de configuración — también le dice al agente qué hacen las 13 herramientas y qué errores evitar (referencias obsoletas, adivinar la sintaxis de agent-browser, intentar distinguir identidades por el título de la página). Se genera a partir de valores de runtime en vivo, por lo que las rutas en él siempre son correctas para la máquina en la que se ejecuta.

O haz clic en un botón

El mismo panel tiene una instalación de un clic que escribe el servidor MCP en ~/.codex/config.toml (idempotente, con una copia de seguridad automática primero). Las rutas se resuelven en tiempo de ejecución desde process.execPath, por lo que no pueden ser incorrectas.

O configúralo manualmente

En macOS:

[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"

El ejecutable bajo Contents/MacOS/ sigue productName, por lo que es Personae. Ten en cuenta que electron-builder's executableName solo se aplica a Windows.

command apunta al binario de la propia aplicación; ELECTRON_RUN_AS_NODE=1 hace que se degrade a un runtime Node simple. Por eso no se requiere instalación de Node (verificado: con PATH establecido a /usr/bin:/bin, el flujo completo sigue funcionando).

Claude Code:

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

Herramientas MCP

Cada herramienta toma un argumento identity (nombre o id).

HerramientaPropósito
load_skillObtener la sintaxis real de comandos de la versión incluida de agent-browser (devuelve un índice de secciones por defecto; pasa section para una específica)
list_identitiesListar todas las identidades con estado abierto y URL actual
open_identityAbrir la ventana de una identidad
snapshotInstantánea del árbol de accesibilidad que devuelve referencias de elementos [ref=eN]
navigateNavegar a una URL
click / fill / pressInteracción; click acepta una referencia o texto visible
actEjecutar varios comandos dentro de un solo proceso agent-browser
get_text / get_urlLeer contenido
screenshotCapturar una captura de pantalla
eval_jsEvaluar JavaScript

Cuando click / fill reciben una referencia @eN, automáticamente toman una instantánea primero, porque las referencias solo son válidas dentro de un solo proceso agent-browser — reutilizar una entre procesos siempre falla con Unknown ref. Para interacciones de varios pasos, colócalas en un lote act.

Controlar una ventana emergente hija. Cuando una página abre una ventana emergente (un inicio de sesión OAuth, un diálogo de compartir…), esa ventana emergente se convierte en una ventana hija que pertenece a la misma identidad (ver Popups y OAuth). Es un objetivo CDP separado, listado bajo la identidad en list_identities como children. Cada herramienta de acción/lectura toma un argumento opcional target — pasa el targetId de una hija (o su índice) para controlar la ventana emergente en lugar de la ventana principal; omítelo para controlar la ventana principal como antes.

Arquitectura

┌─────────────────────────────────────────────────────────┐
│  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   │
                    └────────────────────────┘

Por qué cada ventana de identidad es "un shell más dos WebContentsViews"

El objetivo es envolver páginas de terceros en nuestra propia interfaz de navegación sin usar la etiqueta <webview>. Entonces:

  • el shell BrowserWindow es solo un contenedor y carga about:blank;
  • la barra superior es un chrome.html local con un preload dedicado que expone solo seis métodos de navegación;
  • el área de contenido es un WebContentsView donde se aplica la partición, y deliberadamente no carga ningún preload, dejando las páginas de terceros intactas.

Los BrowserWindows anidados y BrowserView también funcionan, y el agente puede controlar los tres (todo verificado). WebContentsView se eligió porque BrowserView está marcado como @deprecated en las definiciones de tipos de Electron, y porque una ventana anidada es una ventana nativa separada en macOS — arrastrar, redimensionar y minimizar requerirían sincronización manual de límites, que nunca se siente como "una ventana de navegador".

Popups y OAuth

Una página puede abrir una ventana emergente con window.open(url, name, features) — inicios de sesión OAuth, diálogos de "compartir en…", etc. Estas necesitan ser ventanas reales: una ventana emergente OAuth depende de window.opener y postMessage/window.close() para devolver el resultado, por lo que no se puede aplanar en una navegación dentro de la página sin romper el flujo.

El enfoque aquí trata una ventana emergente como un hijo de primera clase de su identidad, sin intentar adivinar "¿es una ventana OAuth?":

  • Misma ventana vs hijo. Un <a target="_blank"> simple o un window.open sin características permanece como una navegación dentro de la ventana (sin opener, sin nuevo objetivo) — eso es seguro y mantiene el invariante de "una identidad, un objetivo principal". Solo una apertura con disposición new-window (es decir, window.open con características de ventana) se convierte en una ventana hija real.
  • Pertenece a la identidad. El hijo se crea con parent establecido a la ventana shell de esa identidad y la misma partición, por lo que flota sobre la ventana correcta y sus cookies caen en la cuenta correcta. window.opener se conserva, por lo que todos los estilos OAuth funcionan. outlivesOpener: false significa que cerrar la identidad cierra sus ventanas emergentes.
  • Bloqueador de popups (activado por defecto). Lo único que distingue "debería abrirse" de "no debería" es la activación del usuario — ¿hubo un clic o pulsación de tecla real en el último segundo? Esto es exactamente cómo funcionan los bloqueadores de popups de los navegadores principales: nunca inspecciona el dominio o el contenido. Los pop-unders impulsados por scripts (sin entrada previa) se bloquean y se muestran como un toast silencioso; puedes desactivar el bloqueador en la barra superior.
  • Los agentes pueden controlarlo. Cada hijo es un objetivo CDP separado, resuelto a su targetId autoritativo y listado bajo la identidad como children. Cada herramienta MCP toma un target opcional para dirigirse a una ventana emergente específica. El contrato de "un objetivo principal por identidad" para el mundo exterior no se rompe — se extiende honestamente a "un principal más hijos rastreados".

La decisión en sí (same-window / allow-child / ignore) vive en una función pura, src/main/window-open.ts, y está probada por unidades.

Proxy por identidad

Cada identidad puede enrutar a través de su propio proxy, algo útil cuando quieres que las cuentas del mismo sitio provengan de diferentes regiones o para probar el comportamiento geográfico. Tú aportas tu propio proxy (cómpralo en IPRoyal / Webshare / Bright Data / quien sea); Personae solo proporciona la configuración y lo conecta.

  • Dónde se aplica. Un proxy se establece en la partición persist: de la identidad mediante session.setProxy, por lo que es genuinamente por identidad: la identidad A puede salir por una IP de EE. UU. mientras que la identidad B sale por Japón. Sin proxy configurado significa conexión directa.
  • Compatibilidad. HTTP, HTTPS y SOCKS5. Puedes rellenar los campos por separado o pegar una cadena completa como socks5://user:pass@host:port en el campo de host.
  • Autenticación. Los proxies con nombre de usuario y contraseña funcionan. Las credenciales nunca van en la cadena de reglas del proxy (Chromium rechaza user:pass@ allí); la autenticación se responde a través del evento login de Electron.
  • Las contraseñas están cifradas en reposo. La contraseña se almacena mediante safeStorage (Keychain en macOS, DPAPI en Windows) en userData/proxy-secrets.json, nunca en identities.json y nunca se envía de vuelta al renderizador. Cuando safeStorage no está disponible, se recurre a texto plano y la interfaz lo indica.
  • Probar conexión. Un botón consulta un servicio de eco de IP a través de la sesión de esa identidad y muestra la IP de salida real, para que puedas confirmar que el proxy está realmente activo.
  • Protección contra fugas WebRTC. Para cualquier identidad con proxy, se aplica setWebRTCIPHandlingPolicy('disable_non_proxied_udp') para que WebRTC no pueda eludir el proxy y filtrar tu IP real.

Esto no es un navegador anti-detección. Cambia tu salida de red, nada más. Más allá de la protección WebRTC mencionada, no realiza suplantación de huellas digitales (Canvas / WebGL / UA / zona horaria / fuentes…), por lo que varias identidades en la misma máquina siguen compartiendo la misma huella del navegador. Si una plataforma correlaciona cuentas por huella, un proxy por sí solo no ocultará eso.

El análisis del proxy (parseProxyInput / buildProxyRules) es una función pura en src/main/proxy.ts y está probado por unidades, incluido que las credenciales nunca se filtren en la cadena de reglas.

Cosas que nos mordieron

Se mantienen aquí porque la mayoría no están documentadas en ningún sitio.

El puerto CDP es por proceso del navegador. --remote-debugging-port es un interruptor a nivel de proceso y no tiene contraparte webPreferences, por lo que una aplicación Electron tiene exactamente un servidor CDP; no puedes dar a cada ventana su propio puerto. Usa el puerto 0 para que el sistema asigne uno y luego léelo de userData/DevToolsActivePort — ese archivo se escribe antes de que el enlace tenga éxito, así que también sondea /json/version para verificar que está activo.

--pin-tab simplemente falla en Electron. Activa Target.createTarget, que Electron informa como Not supported. Peor aún, el fijado es persistente: una vez que una sesión lo ha usado, el estado se guarda e incluso close --all no lo borra, rompiendo todos los comandos posteriores. Así que pasa --no-pin-tab explícitamente en cada llamada.

Un objetivo de página con título vacío cuelga agent-browser. Si la ventana del shell nunca llama a loadURL, aparece en CDP como un objetivo de página con url y título vacíos; adjuntarse a él cuelga para siempre (tab list no devuelve nada, ni siquiera un tiempo de espera). Por eso el shell debe cargar about:blank y establecer un título legible.

Usa about:blank en lugar de data:text/html,<title>... — los títulos no ASCII en este último se decodifican como latin-1 y se convierten en texto corrupto.

Establecer el título también tiene una condición de carrera: about:blank carga tan rápido que did-finish-load puede dispararse antes de que el listener esté adjunto, dejando el título atascado en about:blank cuando varias identidades se abren simultáneamente. Solución: llama a setTitle sincrónicamente una vez y luego de nuevo después de la carga.

Nunca identifiques una ventana por título, url o índice de pestaña. Los títulos y URLs son idénticos cuando varias identidades abren el mismo sitio, y el orden de objetivos CDP no coincide con el orden de índices de pestañas de agent-browser, por lo que los índices tampoco se pueden derivar. Solo targetId funciona.

El contenido de habilidades de agent-browser cambia entre versiones — no copies sintaxis de la web. Su propio SKILL.md establece explícitamente que no contiene sintaxis de comandos y requiere skills get para obtenerla desde la CLI. Este proyecto demostró el punto: tab --url "*settings*" y --pin-tab encontrados en línea no existen en versiones anteriores, mientras que tab list --json emitiendo targetId es una capacidad más nueva — y todo el esquema de direccionamiento de este proyecto se basa en ella.

agent-browser busca hacia arriba desde la ubicación del binario un directorio skills/, lo que puede chocar con el directorio de un proyecto no relacionado con el mismo nombre. Pasa AGENT_BROWSER_SKILLS_DIR explícitamente.

Un binario empaquetado no puede depender solo de asarUnpack. asarUnpack coloca archivos bajo app.asar.unpacked/resources/bin/, mientras que process.resourcesPath apunta a Contents/Resources — no es la misma ubicación, por lo que el binario no se encuentra después del empaquetado. Usa extraResources para alinear las rutas y mantén asarUnpack limitado a archivos realmente importados con ?asset, de lo contrario el mismo binario se envía dos veces.

En macOS no puedes simplemente "omitir la firma". Establecer identity: null hace que electron-builder omita la firma por completo; el bundle conserva la firma adhoc original de Electron, pero los recursos han sido modificados, por lo que la firma y el contenido no coinciden y la aplicación se niega a iniciar silenciosamente (sin error, sin registro; spctl informa code has no resources but signature indicates they must be present). El enfoque correcto es identity: '-' para la firma adhoc más com.apple.security.cs.disable-library-validation en los entitlements — establece tanto entitlements como entitlementsInherit, ya que el primero cubre el proceso principal.

El directorio userData sigue el package.json de name, mientras que el ejecutable sigue productName. Son dos campos diferentes, y executableName solo se aplica a Windows — así que si no coinciden, buscarás en el lugar equivocado. Este proyecto establece deliberadamente ambos en Personae para evitar eso. (Cuidado en Linux: su sistema de archivos distingue mayúsculas y minúsculas, por lo que un directorio creado con un nombre en minúsculas más antiguo no se encontrará.)

publish: generic con una URL de marcador de posición rompe el empaquetado en el último paso. La plantilla de electron-builder incluye provider: generic con url: https://example.com/auto-updates. Cambiar a provider: github hace que intente inferir un canal de lanzamiento al final del empaquetado, y sin contexto de propietario/repositorio lanza TypeError: Cannot read properties of null (reading 'channel') — la compilación ya está completa, pero sale como un fallo. Este proyecto publica mediante gh release upload en CI, así que publish: null es lo que se usa.

Un if a nivel de trabajo en GitHub Actions no puede acceder al contexto matrix. Escribir if: inputs.platforms == matrix.name para filtrar plataformas falla silenciosamente (actionlint marca context "matrix" is not allowed here, pero GitHub mismo no se queja), por lo que cada plataforma se compila independientemente de la entrada. Genera el JSON de la matriz en un trabajo anterior y pásalo a strategy.matrix con fromJSON.

Al renderizar SVG con Electron, suprime window-all-closed. El script de iconos itera "crear ventana → capturar → destruir → crear siguiente", y cada destroy() reduce el contador de ventanas a cero, lo que por defecto cierra toda la aplicación — el síntoma es un bloqueo después de solo el primer tamaño, con el hijo informando No rendezvous client, terminating process (parent died?). Parece un problema de sincronización, pero reintentar no ayuda. Registra un manejador window-all-closed vacío.

Dos más: en pantallas Retina capturePage genera a devicePixelRatio (pedir 512 produce 1024), así que fija zoomFactor / deviceScaleFactor; e incrustar un SVG moderadamente largo en data:text/html;base64,... excede el límite de longitud de URL y hace que loadURL falle con ERR_FAILED (-2), así que usa un archivo temporal con una referencia file://.

Limitaciones conocidas y notas de seguridad

  • El aislamiento es una convención, no un límite arquitectónico. El puerto CDP no tiene control de acceso. Solo escucha en loopback con un puerto aleatorio, pero cualquier proceso local que se conecte puede controlar todas las identidades, cruzando los límites de partición. Ese es el costo directo de permitir que agentes externos se adjunten. No manejes cuentas sensibles en una máquina compartida.
  • Navegación en la misma ventana para enlaces ordinarios. setWindowOpenHandler convierte los enlaces window.open y target="_blank" en navegación en la misma ventana para que una identidad siempre se mapee a un objetivo principal. Las ventanas emergentes genuinas (window.open con características, por ejemplo OAuth) son la excepción — se abren como ventanas hijas rastreadas; ver Popups y OAuth.
  • Cada identidad ocupa al menos 3 objetivos de página CDP (shell + barra superior + contenido), más uno adicional por cada ventana emergente hija abierta, por lo que escala aproximadamente a 3× el número de identidades.
  • La versión empaquetada de agent-browser está fijada en el momento de la compilación; las correcciones posteriores no se recogen automáticamente.
  • El comportamiento en tiempo de ejecución solo se verifica en macOS (arm64), incluida la compilación empaquetada. El empaquetado de Windows tiene éxito en CI (macOS y Windows se compilan y producen instaladores), pero la aplicación nunca se ha ejecutado realmente en Windows, por lo que el comportamiento en tiempo de ejecución allí no está verificado. Linux no es un objetivo de compilación. bundle:ab empaqueta solo para la plataforma actual.
  • Las compilaciones con firma adhoc no son distribuibles: solo se ejecutan en la máquina de compilación y están bloqueadas por Gatekeeper en otros lugares. La distribución real necesita tu propio certificado y notarización.
  • El lado de Codex no se ha verificado con un cliente real: el flujo MCP se probó con un script actuando como cliente (incluida la compilación empaquetada en un entorno sin Node), pero nunca contra una ejecución real de codex.
  • El soporte de proxy solo cambia la salida de red, no la huella. Ver Proxy por identidad — no es un navegador anti-detección.

Estructura del proyecto

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 registration
src/shared/
  colors.ts       identity palette — imported by BOTH main and renderer,
                  so a window's top-bar dot always matches its list entry
src/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 / ico
test/
  *.test.ts                node:test unit tests for the pure modules above
design/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/ y resources/skills/ son generados por pnpm bundle:ab y no se confirman.

Icono

La fuente es design/logo/icon.svg — tres tarjetas de colores no superpuestas para tres identidades aisladas, con un puntero para el control del agente. Después de editar el SVG, regenera cada formato de plataforma:

pnpm icons

Esto produce build/icon.png (1024), build/icon.icns, build/icon.ico y resources/icon.png (512, el icono de ventana en tiempo de ejecución).

El renderizado pasa por el Chromium integrado de Electron, por lo que no se necesitan rsvg-convert / Inkscape / ImageMagick — esos suelen estar ausentes en un entorno limpio, mientras que Electron es una dependencia que este proyecto tiene de todos modos. .icns se ensambla con el iconutil del sistema; .ico se escribe byte a byte.

Publicación

Ambos flujos de trabajo son solo manuales (workflow_dispatch) y nunca se ejecutan en push o tag.

Ve a Actions en GitHub, elige un flujo de trabajo y luego Run workflow:

Flujo de trabajoPropósitoEntradas
Build & ReleaseCompilar macOS + Windows, crear una Releaseversión (opcional), plataforma (all / macos / windows), crear release, prerelease
Checklint + typecheck + prueba de empaquetadosi ejecutar el empaquetado

La Release se crea como borrador; revisa los activos y luego pulsa Publish tú mismo. Si el tag ya existe, los activos se añaden a él (--clobber sobrescribe archivos con el mismo nombre), por lo que las re-ejecuciones no simplemente fallan.

La publicación no usa el paso de publicación propio de electron-builder (publish: null en electron-builder.yml); un solo trabajo de agregación sube todo con gh release upload en su lugar, porque tres plataformas ejecutándose en paralelo intentarían cada una crear la misma Release y se pisarían entre sí. Cuando el flujo de trabajo Build & Release crea una versión, también publica el lanzador stdio @leek-emperor/personae-mcp con la versión correspondiente en npm y luego publica server.json con la CLI oficial mcp-publisher. Establece el secreto NPM_TOKEN del repositorio (un token de automatización de npm con permiso para publicar ese paquete público) antes de la primera versión; la autenticación del Registro MCP usa OIDC de GitHub y no necesita un secreto separado.

Los artefactos de macOS de CI también están firmados ad-hoc. Para una firma real, agrega CSC_LINK / CSC_KEY_PASSWORD a los secretos del repositorio y establece notarize a true en electron-builder.yml.

Licencia

MIT — consulta LICENSE.

El agent-browser incluido tiene licencia Apache-2.0; los derechos de autor pertenecen a sus autores.