Personae MCP
O Personae permite que agentes de IA usem MCP para controlar com segurança uma identidade de navegador específica já aberta e isolada por conta—sem misturar sessões ou abrir um navegador separado.
Documentação
Personae
Um navegador multi-identidade · cada identidade é uma partição isolada, cada janela é controlável por agente
Inclui navegador de agente e um servidor MCP — sem necessidade de instalar Node ou CLI
English · 中文
Um navegador desktop em Electron que combina duas coisas: isolamento multi-conta e permitir que agentes de IA controlem o navegador.
- Cada identidade de navegador é sua própria partição
persist:do Chromium — cookies, localStorage e estado de login são totalmente isolados, então você pode estar conectado ao mesmo site em várias contas ao mesmo tempo. - Cada identidade é uma janela independente com botões de voltar/avançar e uma barra de endereço. Ela se comporta como um navegador comum.
- O binário agent-browser está incluído, e essas janelas são expostas via MCP, para que Codex / Claude Code possam controlar a página de uma identidade específica.
Os usuários não precisam pré-instalar Node, Chrome for Testing ou qualquer CLI.
Registro MCP
O Personae é publicado no Registro MCP oficial
como io.github.leek-emperor/personae após seu primeiro lançamento compatível com o Registro.
O pacote do Registro é um pequeno lançador stdio
para um aplicativo desktop Personae instalado separadamente e em execução — ele não
substitui o aplicativo de navegador nem cria um navegador em nuvem.
Após instalar e iniciar o Personae, um cliente que suporte pacotes MCP npm pode executar:
npx -y @leek-emperor/personae-mcp
O lançador descobre o Personae por meio de sua ponte local e mantém cada chamada de ferramenta limitada à identidade nomeada na solicitação. Ele não usa chave de API e não abre nenhum listener de rede público. O controle Configure Codex do aplicativo continua sendo o caminho de configuração recomendado, pois usa o runtime Electron incluído e não exige instalação do Node.
O problema que ele resolve
As ferramentas existentes de automação de navegador assumem que o agente inicia o navegador. Se o seu produto é um cliente de navegador, é o contrário: as janelas já existem e pertencem a diferentes identidades de conta. O que o agente precisa é anexar sem cruzar identidades.
A abordagem aqui:
- O aplicativo abre uma porta CDP na inicialização (atribuída pelo sistema, somente loopback).
- Quando uma janela de identidade abre, o processo principal resolve o targetId autoritativo da página de conteúdo via
Target.getTargetInfo. - Uma ponte local expõe o mapeamento
identity → targetId. - O servidor MCP usa esse targetId diretamente como referência de aba — um acerto em uma única etapa.
Assim, snapshot(identity: "Account A") e snapshot(identity: "Account B") sempre chegam à janela certa, mesmo quando ambas têm o mesmo site aberto com títulos e URLs idênticos.
Início rápido
Requer Node ≥ 20 e pnpm (apenas para desenvolvimento; o aplicativo empacotado não depende de nenhum dos dois).
agent-browser deve ser ≥ 0.36. Abaixo disso, tab list --json não emite targetId,
que é a única coisa que vincula uma identidade à sua janela — toda ferramenta falharia ao encontrar seu
alvo. A versão em package.json já está fixada; não a rebaixe.
pnpm install
pnpm bundle:ab # copy the agent-browser binary and core skill into resources/
pnpm dev
Adicione uma ou duas identidades de navegador na interface e clique em uma identidade para abrir sua janela.
A interface está em inglês por padrão. Use o alternador EN / 中 no canto superior direito para mudar para chinês; a escolha é lembrada e também se aplica à barra de ferramentas dentro de cada janela de identidade e ao prompt de agente que você copia.
Testes
As decisões complicadas e pouco documentadas vivem em pequenos módulos puros para que possam ser
protegidas por testes. Eles rodam em node:test sem dependências extras:
pnpm test
Coberto: a decisão de window.open / popup (window-open.ts), o parsing de entrada
de proxy (proxy.ts), a normalização de URL da barra de endereço (url-input.ts), a
junção do TOML de configuração do Codex (toml.ts) e a paleta de identidades (colors.ts).
Empacotamento:
pnpm build:mac # or build:win
O padrão é assinatura adhoc (identity: '-'), que não exige certificado de desenvolvedor Apple e funciona bem na máquina de build. Mas um aplicativo com assinatura adhoc não pode ser distribuído — o Gatekeeper o bloqueará no Mac de outra pessoa. Para distribuição real, forneça um certificado por meio de variáveis de ambiente e defina notarize como true:
CSC_LINK=/path/to/cert.p12 CSC_KEY_PASSWORD=... \
APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=... \
pnpm build:mac
Conectando Codex / Claude Code
O caminho mais rápido: deixe o agente se conectar sozinho
A interface tem um bloco "Let the agent connect itself" que renderiza um prompt pronto para envio. Copie-o, cole-o no Codex ou Claude Code, e o agente escreverá sua própria configuração MCP, reiniciará e verificará o link chamando list_identities.
O prompt não é apenas um trecho de configuração — ele também informa ao agente o que as 13 ferramentas fazem e quais erros evitar (referências obsoletas, adivinhar a sintaxe de agent-browser, tentar distinguir identidades pelo título da página). Ele é gerado a partir de valores de runtime ao vivo, então os caminhos nele estão sempre corretos para a máquina em que está sendo executado.
Ou clique em um botão
O mesmo painel tem uma instalação com um clique que grava o servidor MCP em ~/.codex/config.toml (idempotente, com backup automático primeiro). Os caminhos são resolvidos em runtime a partir de process.execPath, então não podem estar errados.
Ou configure manualmente
No 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"
O executável em Contents/MacOS/ segue productName, então é Personae. Observe que o electron-builder de executableName se aplica apenas ao Windows.
command aponta para o binário do próprio aplicativo; ELECTRON_RUN_AS_NODE=1 faz com que ele degrade para um runtime Node simples. É por isso que nenhuma instalação do Node é necessária (verificado: com PATH definido como /usr/bin:/bin, o fluxo completo ainda funciona).
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
Ferramentas MCP
Toda ferramenta recebe um argumento identity (nome ou id).
| Ferramenta | Propósito |
|---|---|
load_skill | Busca a sintaxe real de comando da versão incluída do agent-browser (retorna um índice de seções por padrão; passe section para uma específica) |
list_identities | Lista todas as identidades com estado de abertura e URL atual |
open_identity | Abre a janela de uma identidade |
snapshot | Snapshot da árvore de acessibilidade retornando referências de elemento [ref=eN] |
navigate | Navega para uma URL |
click / fill / press | Interação; click aceita uma referência ou texto visível |
act | Executa vários comandos dentro de um único processo agent-browser |
get_text / get_url | Lê conteúdo |
screenshot | Captura uma captura de tela |
eval_js | Avalia JavaScript |
Quando click / fill recebem uma referência @eN, eles automaticamente tiram um snapshot primeiro, porque as referências são válidas apenas dentro de um único processo agent-browser — reutilizar uma entre processos sempre falha com Unknown ref. Para interações de várias etapas, coloque-as em um único lote act.
Controlando uma janela filha popup. Quando uma página abre um popup (um login OAuth, um diálogo de compartilhamento…), esse popup se torna uma janela filha pertencente à mesma identidade (veja Popups e OAuth). É um alvo CDP separado, listado sob a identidade em list_identities como children. Toda ferramenta de ação/leitura aceita um argumento opcional target — passe o targetId de um filho (ou seu índice) para controlar o popup em vez da janela principal; omita-o para controlar a janela principal como antes.
Arquitetura
┌─────────────────────────────────────────────────────────┐
│ 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 que cada janela de identidade é "um shell mais duas WebContentsViews"
O objetivo é envolver páginas de terceiros em nossa própria interface de navegação sem usar a tag <webview>. Então:
- o shell
BrowserWindowé apenas um contêiner e carregaabout:blank; - a barra superior é um
chrome.htmllocal com um preload dedicado expondo apenas seis métodos de navegação; - a área de conteúdo é um
WebContentsViewonde a partição se aplica, e ela deliberadamente não carrega nenhum preload, mantendo as páginas de terceiros intocadas.
BrowserWindows aninhados e BrowserView também funcionam, e o agente pode controlar todos os três (tudo verificado). WebContentsView foi escolhido porque BrowserView é marcado como @deprecated nas definições de tipo do Electron, e porque uma janela aninhada é uma janela nativa separada no macOS — arrastar, redimensionar e minimizar exigiriam sincronização manual de limites, o que nunca parece "uma janela de navegador".
Popups e OAuth
Uma página pode abrir um popup com window.open(url, name, features) — logins OAuth, diálogos "compartilhar com…" e assim por diante. Eles precisam ser janelas reais: um popup OAuth depende de window.opener e postMessage/window.close() para devolver o resultado, então não pode ser achatado em uma navegação na página sem quebrar o fluxo.
A abordagem aqui trata um popup como um filho de primeira classe de sua identidade, sem nunca tentar adivinhar "isso é uma janela OAuth?":
- Mesma janela vs filho. Um
<a target="_blank">simples ou umwindow.opensem recursos permanece uma navegação na janela (sem opener, sem novo alvo) — isso é seguro e mantém o invariante "uma identidade, um alvo principal". Apenas uma abertura com disposiçãonew-window(ou seja,window.opencom recursos de janela) se torna uma janela filha real. - Pertence à identidade. O filho é criado com
parentdefinido para a janela shell dessa identidade e a mesma partição, então ele flutua sobre a janela certa e seus cookies caem na conta certa.window.openeré preservado, então todo estilo OAuth funciona.outlivesOpener: falsesignifica que fechar a identidade fecha seus popups. - Bloqueador de popup (ativado por padrão). A única coisa que distingue "deve abrir" de "não deve" é ativação do usuário — houve um clique ou pressionamento de tecla real no último segundo? Isso é exatamente como os bloqueadores de popup dos navegadores convencionais funcionam: nunca inspeciona o domínio ou o conteúdo. Pop-unders dirigidos por script (sem entrada anterior) são bloqueados e exibidos como um toast discreto; você pode desativar o bloqueador na barra superior.
- Agentes podem controlá-lo. Cada filho é um alvo CDP separado, resolvido para seu
targetIdautoritativo e listado sob a identidade comochildren. Toda ferramenta MCP aceita umtargetopcional para endereçar um popup específico. O contrato "um alvo principal por identidade" para o mundo externo não é quebrado — é honestamente estendido para "um principal mais filhos rastreados".
A decisão em si (same-window / allow-child / ignore) vive em uma função pura, src/main/window-open.ts, e é testada por unidade.
Proxy por identidade
Cada identidade pode rotear pelo seu próprio proxy — útil quando você quer que as contas do mesmo site venham de regiões diferentes, ou para testar comportamento geográfico. Você traz seu próprio proxy (compre da IPRoyal / Webshare / Bright Data / quem quiser); o Personae MCP apenas fornece a configuração e faz a conexão.
- Onde se aplica. Um proxy é definido na partição
persist:da identidade viasession.setProxy, então é genuinamente por identidade — a identidade A pode sair por um IP dos EUA enquanto a identidade B sai pelo Japão. Sem proxy definido significa conexão direta. - Suportado. HTTP, HTTPS e SOCKS5. Você pode preencher os campos separadamente, ou colar uma string completa como
socks5://user:pass@host:portno campo de host. - Autenticação. Proxies com usuário/senha funcionam. As credenciais nunca entram na string de regras do proxy (o Chromium rejeita
user:pass@ali); a autenticação é respondida pelo eventologindo Electron. - Senhas são criptografadas em repouso. A senha é armazenada via
safeStorage(Keychain no macOS, DPAPI no Windows) emuserData/proxy-secrets.json, nunca emidentities.jsone nunca enviada de volta ao renderer. OndesafeStoragenão está disponível, ela cai para texto simples e a interface avisa. - Testar conexão. Um botão busca um serviço de eco de IP através da sessão daquela identidade e mostra o IP real de saída, para você confirmar que o proxy está realmente em efeito.
- Proteção contra vazamento WebRTC. Para qualquer identidade com proxy,
setWebRTCIPHandlingPolicy('disable_non_proxied_udp')é aplicado para que o WebRTC não possa contornar o proxy e vazar seu IP real.
Isto não é um navegador anti-detecção. Ele muda sua saída de rede, nada mais. Além da proteção WebRTC acima, ele não faz spoofing de impressão digital (Canvas / WebGL / UA / timezone / fontes…), então várias identidades na mesma máquina ainda compartilham a mesma impressão digital do navegador. Se uma plataforma correlaciona contas por impressão digital, um proxy sozinho não esconderá isso.
O parsing de proxy (parseProxyInput / buildProxyRules) é uma função pura em src/main/proxy.ts e é testado por unidade, incluindo que credenciais nunca vazam para a string de regras.
Coisas que nos morderam
Mantido aqui porque a maioria não está documentada em lugar nenhum.
A porta CDP é por processo do navegador. --remote-debugging-port é uma opção de nível de processo e não tem contraparte webPreferences, então um app Electron tem exatamente um servidor CDP; você não pode dar a cada janela sua própria porta. Use a porta 0 para deixar o sistema atribuir uma, e então leia de volta de userData/DevToolsActivePort — esse arquivo é escrito antes do bind ter sucesso, então também verifique /json/version para saber se está ativo.
--pin-tab simplesmente falha no Electron. Ele dispara Target.createTarget, que o Electron reporta como Not supported. Pior, o pinning é persistente: uma vez que uma sessão o usou, o estado é persistido e até close --all não o limpa, quebrando todos os comandos posteriores. Então passe --no-pin-tab explicitamente em cada chamada.
Um target de página com título vazio trava o agent-browser. Se a janela shell nunca chama loadURL, ela aparece no CDP como um target de página com url e título vazios; anexar a ela trava para sempre (tab list não retorna nada, nem mesmo um timeout). Por isso o shell deve carregar about:blank e definir um título legível.
Use about:blank em vez de data:text/html,<title>... — títulos não-ASCII neste último são decodificados como latin-1 e viram mojibake.
Definir o título também tem uma corrida: about:blank carrega tão rápido que did-finish-load pode disparar antes do listener ser anexado, deixando o título preso em about:blank quando várias identidades abrem concorrentemente. Correção: chame setTitle sincronamente uma vez, e depois novamente após o load.
Nunca identifique uma janela por título, url ou índice de aba. Títulos e URLs são idênticos quando várias identidades abrem o mesmo site, e a ordem dos targets CDP não corresponde à ordem das abas do agent-browser, então os índices também não podem ser derivados. Apenas targetId funciona.
O conteúdo das skills do agent-browser muda entre versões — não copie sintaxe da web. O próprio SKILL.md declara explicitamente que não contém sintaxe de comandos e requer skills get para buscá-la do CLI. Este projeto provou o ponto: tab --url "*settings*" e --pin-tab encontrados online não existem em versões anteriores, enquanto tab list --json emitindo targetId é uma capacidade mais nova — e todo o esquema de targeting deste projeto é construído sobre ela.
O agent-browser busca para cima a partir da localização do binário por um diretório skills/, o que pode colidir com o diretório de um projeto não relacionado com o mesmo nome. Passe AGENT_BROWSER_SKILLS_DIR explicitamente.
Um binário empacotado não pode depender apenas de asarUnpack. asarUnpack coloca arquivos sob app.asar.unpacked/resources/bin/, enquanto process.resourcesPath aponta para Contents/Resources — não é o mesmo local, então o binário não é encontrado após o empacotamento. Use extraResources para alinhar os caminhos, e mantenha asarUnpack limitado a arquivos realmente importados com ?asset, caso contrário o mesmo binário é enviado duas vezes.
No macOS você não pode simplesmente "pular a assinatura". Definir identity: null faz o electron-builder pular a assinatura completamente; o bundle mantém a assinatura adhoc original do Electron, mas os recursos foram modificados, então assinatura e conteúdo discordam e o app se recusa a iniciar silenciosamente (sem erro, sem log; spctl reporta code has no resources but signature indicates they must be present). A abordagem correta é identity: '-' para assinatura adhoc mais com.apple.security.cs.disable-library-validation nos entitlements — defina ambos entitlements e entitlementsInherit, já que o primeiro cobre o processo principal.
O diretório userData segue o package.json do name, enquanto o executável segue o productName. São dois campos diferentes, e executableName só se aplica ao Windows — então se eles discordarem, você procurará no lugar errado. Este projeto define deliberadamente ambos para Personae para evitar isso. (Cuidado no Linux: o sistema de arquivos é sensível a maiúsculas/minúsculas, então um diretório criado com um nome antigo em minúsculas não será encontrado.)
publish: generic com uma URL de placeholder quebra o empacotamento no último passo. O template do electron-builder envia provider: generic com url: https://example.com/auto-updates. Mudar para provider: github faz ele tentar inferir um canal de release no final do empacotamento, e sem contexto de owner/repo ele lança TypeError: Cannot read properties of null (reading 'channel') — o build já está completo, mas sai como falha. Este projeto publica via gh release upload no CI, então publish: null é o que vale.
Um if de nível de job no GitHub Actions não pode acessar o contexto matrix. Escrever if: inputs.platforms == matrix.name para filtrar plataformas falha silenciosamente (actionlint sinaliza context "matrix" is not allowed here, mas o próprio GitHub não reclama), então toda plataforma é construída independentemente da entrada. Gere o JSON da matriz em um job anterior e alimente-o para strategy.matrix com fromJSON.
Ao renderizar SVG com Electron, suprima window-all-closed. O script de ícones faz um loop "criar janela → capturar → destruir → criar próxima", e cada destroy() reduz a contagem de janelas a zero, o que por padrão encerra o app inteiro — o sintoma é um crash após apenas o primeiro tamanho, com o filho reportando No rendezvous client, terminating process (parent died?). Parece um problema de timing, mas tentar novamente não ajuda. Registre um handler window-all-closed vazio.
Mais dois: em displays Retina, capturePage gera em devicePixelRatio (pedir 512 resulta em 1024), então fixe zoomFactor / deviceScaleFactor; e embutir um SVG moderadamente longo em data:text/html;base64,... excede o limite de comprimento de URL e faz loadURL falhar com ERR_FAILED (-2), então use um arquivo temporário com uma referência file://.
Limitações conhecidas e notas de segurança
- Isolamento é uma convenção, não uma fronteira arquitetural. A porta CDP não tem controle de acesso. Ela só escuta em loopback com uma porta aleatória, mas qualquer processo local que conecte pode dirigir todas as identidades, cruzando fronteiras de partição. Esse é o custo direto de permitir que agentes externos se anexem. Não lide com contas sensíveis em uma máquina compartilhada.
- Navegação na mesma janela para links comuns.
setWindowOpenHandlertransforma linkswindow.openetarget="_blank"em navegação na mesma janela para que uma identidade sempre mapeie para um target principal. Popups genuínos (window.opencom features, ex.: OAuth) são a exceção — eles abrem como janelas filhas rastreadas; veja Popups e OAuth. - Cada identidade ocupa pelo menos 3 targets de página CDP (shell + barra superior + conteúdo), mais um por popup filho aberto, então escala aproximadamente 3× o número de identidades.
- A versão do agent-browser empacotada é fixada no momento do build; correções upstream não são incorporadas automaticamente.
- O comportamento em runtime só é verificado no macOS (arm64), incluindo o build empacotado. O empacotamento para Windows tem sucesso no CI (macOS e Windows são ambos construídos e produzem instaladores), mas o app nunca foi realmente executado no Windows, então o comportamento em runtime lá não é verificado. Linux não é um alvo de build.
bundle:abempacota apenas para a plataforma atual. - Builds com assinatura adhoc não são distribuíveis: eles rodam apenas na máquina de build e são bloqueados pelo Gatekeeper em outros lugares. Distribuição real precisa do seu próprio certificado e notarização.
- O lado Codex não foi verificado com um cliente real: o fluxo MCP foi testado com um script atuando como cliente (incluindo o build empacotado em um ambiente sem Node), mas nunca contra uma execução real de codex.
- O suporte a proxy apenas muda a saída de rede, não a impressão digital. Veja Proxy por identidade — não é um navegador anti-detecção.
Estrutura do projeto
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/ e resources/skills/ são gerados por pnpm bundle:ab e não são commitados.
Ícone
A fonte é design/logo/icon.svg — três cartões coloridos não sobrepostos para três identidades isoladas, com um ponteiro para controle do agente. Após editar o SVG, regenere cada formato de plataforma:
pnpm icons
Isso produz build/icon.png (1024), build/icon.icns, build/icon.ico e resources/icon.png (512, o ícone de janela em runtime).
A renderização passa pelo Chromium embutido do Electron, então rsvg-convert / Inkscape / ImageMagick não são necessários — eles geralmente estão ausentes em um ambiente limpo, enquanto o Electron é uma dependência que este projeto já tem. .icns é montado pelo iconutil do sistema; .ico é escrito byte a byte.
Lançamento
Ambos os workflows são apenas manuais (workflow_dispatch) e nunca rodam em push ou tag.
Vá para Actions no GitHub, escolha um workflow, depois Run workflow:
| Workflow | Propósito | Entradas |
|---|---|---|
| Build & Release | Build macOS + Windows, criar um Release | versão (opcional), plataforma (all / macos / windows), criar release, prerelease |
| Check | lint + typecheck + teste de fumaça de empacotamento | se deve rodar o empacotamento |
O Release é criado como rascunho; revise os assets, depois clique em Publish você mesmo. Se a tag já existir, os assets são anexados a ela (--clobber sobrescreve arquivos de mesmo nome), então re-execuções não apenas falham.
A publicação não usa o passo de publish do próprio electron-builder (publish: null em electron-builder.yml); um job de agregação único envia tudo com gh release upload em vez disso, porque três plataformas rodando em paralelo tentariam cada uma criar o mesmo Release e se sobrescreveriam.
Quando o workflow Build & Release cria um release, ele também publica o
launcher stdio @leek-emperor/personae-mcp correspondente à versão no npm e, em seguida,
publica server.json com a CLI oficial mcp-publisher. Defina o
segredo NPM_TOKEN do repositório (um token de automação do npm com permissão para
publicar esse pacote público) antes do primeiro release; a autenticação do MCP Registry
usa OIDC do GitHub e não precisa de segredo separado.
Artefatos macOS do CI também são assinados ad-hoc. Para assinatura real, adicione CSC_LINK / CSC_KEY_PASSWORD aos segredos do repositório e defina notarize como true em electron-builder.yml.
Licença
MIT — veja LICENSE.
O agent-browser incluído é Apache-2.0; os direitos autorais pertencem aos seus autores.