Legion MCP

Conselhos de LLM nativos de MCP para debates, júris, painéis cegos, votação, refinamento e deliberação altamente personalizável com múltiplos modelos.

Documentação

Legion

"Eu sou Legion, pois somos muitos."

Um conselho de modelos nativo de MCP. O Legion expõe LLMs como ferramentas individuais e os orquestra em debates, júris, painéis cegos, desafios privados de refinamento, workshops e deliberações personalizadas com múltiplos modelos.

Cada modelo é acessado através do formato de wire da Responses API da OpenAI. Use OpenAI ou Azure diretamente, roteie outros provedores através de um gateway compatível (como um proxy LiteLLM) e configure todo o conselho através de arquivos com recarga a quente.

Conteúdo

Como funciona

flowchart LR
   AI[Calling AI] -->|claude / gpt / gemini …| Legion
   Legion -->|Responses API| GPT[OpenAI / Azure — direct]
   Legion -->|Responses API| GW[Gateway e.g. LiteLLM]
   GW --> Claude & Gemini & Llama
  • Uma ferramenta por modelo, nomeada após o nome do modelo em formato slug (ex.: Claude → claude). Cada uma aceita um prompt além de context, role, system, temperature e maxTokens opcionais.
  • Uma ferramenta quorum distribui um prompt para vários modelos — com papéis, discussão em múltiplas rodadas, modos de visibilidade e síntese — e retorna cada resposta separadamente. Veja Presets para as opções de orquestração.
  • Presets são conselhos nomeados e pré-configurados (debate, tribunal, revisão de código, …), cada um exposto como sua própria ferramenta.
  • Identidade e telemetria viajam em structuredContent, não no texto da resposta. O registro de logs vai para stderr (seguro para stdio).

Decisões de design

  • Sem adaptadores de provedor. Não há código específico de provedor nem lista de modelos embutida. O Legion fala um único formato de wire; modelos que não o falam nativamente passam por um gateway. Suportar um novo modelo não exige mudanças aqui.
  • Modelos são configuração, não código. Adicionar um modelo significa adicionar um arquivo JSON. Via HTTP, o diretório é relido a cada requisição, então não há rebuild ou reinício; via stdio, a configuração é lida uma vez por conexão (veja Recarga a quente).
  • Uma ferramenta por modelo. Cada modelo aparece para a IA chamadora como sua própria ferramenta com sua própria descrição, em vez de uma única ferramenta com um parâmetro de modelo. A ferramenta quorum cobre o caso ad-hoc de múltiplos modelos, e cada preset em config/presets/ é exposto como sua própria ferramenta de conselho pré-configurada e obrigatória.
  • Sem estado. Cada chamada é única com store: false. Nada é persistido, então não há banco de dados nem estado de conversa para gerenciar.
  • Pequeno. Alguns milhares de linhas de TypeScript, um único arquivo de saída empacotado, cinco dependências.

Requisitos

  • Node.js 24+
  • Pelo menos um endpoint compatível com OpenAI-Responses (uma API de provedor diretamente, ou um gateway como LiteLLM para modelos que precisam de ponte)

Configuração

Do npm — sem clone, sem build:

npx legion-mcp

De um clone:

npm install
copy .env.example .env   # then edit .env

Configuração

Toda a configuração vive em um diretório config/. Os padrões empacotados são sempre a camada base; uma pasta config/ no diretório de trabalho atual é sobreposta sobre eles, por arquivo:

  • Recursos de diretório (models/, roles/, presets/, tools/): um arquivo local substitui o arquivo empacotado de mesmo nome; um arquivo apenas local é adicionado; todo arquivo empacotado que você não tocar permanece. Então, colocar um config/presets/refine.json substitui apenas aquele preset — os outros presets empacotados permanecem.
  • Texto de arquivo único (prompts.json, errors.json, schema.json): mesclado por chave — empacotado < local. Um arquivo local parcial substitui apenas as chaves que define.
  • description.md: local vence inteiro se presente, senão o empacotado.

A sobreposição pode substituir ou adicionar, mas não excluir uma entrada empacotada. Para escolher quais presets empacotados são registrados como ferramentas, use PRESETS (veja abaixo).

Instalando do npm? Você deve fornecer seus próprios arquivos de modelo. A configuração empacotada inclui apenas arquivos de modelo *.example.json sem chave, que o scanner deliberadamente ignora — então o pacote contribui com zero modelos reais. Sem um arquivo de modelo real, o servidor falha rapidamente na inicialização (No model files found in ...). Drop one config/models/<name>.json ao lado de onde você executa o servidor (veja abaixo) — o restante recai nos padrões empacotados.

O layout abaixo é idêntico de qualquer forma.

Recarga a quente

A configuração é lida novamente quando uma instância do servidor é construída, nunca gravada em tempo de build — mas quando isso acontece depende do transporte:

TransporteA configuração é relidaEfeito de editar um arquivo de configuração
httppor requisiçãoAo vivo na próxima chamada, sem reinício.
stdiouma vez por conexãoTem efeito quando o cliente reconecta.

O HTTP constrói um servidor novo por requisição, então um modelo, papel, preset ou substituição de texto adicionado é captado na próxima chamada. O stdio fixa um servidor para a vida da conexão, então a mesma edição chega quando o cliente reinicia o Legion — o que para a maioria dos clientes MCP de desktop significa recarregar o servidor, não reiniciar a máquina.

Onde este README diz que algo está "ao vivo na próxima chamada", leia como o comportamento HTTP; via stdio é a próxima conexão.

Modelos — config/models/*.json

Pelo menos um arquivo de modelo é obrigatório — o servidor falha rapidamente sem um. Cada arquivo JSON se torna uma ferramenta, nomeada após o nome do arquivo em formato slug (config/models/fable.json → ferramenta fable):

{
   "model": "claude-fable-5",
   "description": "Claude Fable — fast, creative, general purpose.",
   "baseUrl": "https://api.example.com",
   "apiKey": "sk-optional-per-model-key"
}
  • model (obrigatório) — o id do modelo implantado para o qual o endpoint roteia.
  • description — ajuda a IA chamadora a escolher o modelo certo.
  • system — instruções de sistema de linha de base opcionais embutidas em cada chamada para este modelo.
  • baseUrl / apiKey — opcionais; valores omitidos recaem em DEFAULT_BASE_URL / DEFAULT_API_KEY. apiKey pode referenciar uma variável de ambiente com o prefixo env: — "apiKey": "env:GPT_KEY" lê GPT_KEY do ambiente (não definido/vazio é um erro fatal de inicialização), então uma chave pode ficar fora do arquivo inteiramente.
  • omitParams — lista opcional de parâmetros de requisição a descartar para este modelo, ex.: ["temperature"]. O servidor permanece agnóstico de provedor: ele nunca assume quais modelos rejeitam quais parâmetros — você declara as peculiaridades de cada modelo aqui. Útil para modelos de raciocínio e algumas implantações que rejeitam temperature.
  • reasoning — "minimal" | "low" | "medium" | "high" opcional, enviado como reasoning.effort. Modelos de raciocínio podem gastar um maxTokens inteiro pensando e não emitir nada; "low" pode ajudar a deixar espaço para uma resposta. O endpoint deve suportar este parâmetro; ele não contorna a filtragem de conteúdo.
  • maxTokens — teto de saída rígido opcional para este modelo. Cada requisição envia min(requested, ceiling), seja o que o chamador ou o papel do preset pediu. Para modelos que pensam mais quanto mais espaço têm e voltam vazios.

Adição a quente: o diretório é reescaneado por requisição — adicione ou edite um arquivo de modelo e ele estará ao vivo na próxima chamada, sem reinício.

Segredos e git: arquivos de modelo podem conter chaves de API, então config/models/*.json é ignorado pelo git. Copie um *.example.json (rastreado, sem chave, ignorado pelo scanner) para começar:

copy config\models\gpt.example.json config\models\gpt.json   # then add your key

Papéis — config/roles/*.md

Arquivos de instrução opcionais com adição a quente. Cada arquivo .md se torna um papel nomeado (slugificado a partir do nome do arquivo). Coloque um arquivo, ele está ao vivo na próxima chamada. Este repositório inclui skeptic.md, builder.md, judge.md e short.md (um papel sucinto de "responda imediatamente, sem deliberação" útil para turnos com saída restrita) como iniciadores prontos para uso — edite ou exclua livremente (eles não contêm segredos).

Seletores disponíveis nas ferramentas se tornam roleName, ex.: passar role: "skeptic" ou usar "model:skeptic" em quorum.models.

Presets — config/presets/*.json

Receitas de conselho opcionais com adição a quente, um arquivo JSON por preset (nomeado após o nome do arquivo em formato slug, como modelos). Cada preset se torna sua própria ferramenta — coloque config/presets/code_review.json e uma ferramenta code_review aparece na próxima requisição. Cada preset tem um description, uma lista de roles e padrões opcionais autoritativos de mode / synthesizer. Cada papel define seu comportamento inline — o description de um papel é suas instruções (o contrato de comportamento); um papel sem description recai em um arquivo config/roles/<role>.md correspondente:

{
   "description": [
      "Free-for-all: pit several contestants against each other, then crown a winner.",
      "",
      "Staff `contestant` with as many models as you like; one `judge` decides."
   ],
   "mode": "parallel",
   "synthesizer": "judge",
   "roles": [
      { "role": "contestant", "description": "Argue why your answer beats the others.", "min": 2, "max": null },
      { "role": "judge",      "description": "Crown a single winner and justify it.", "min": 1, "max": 1 }
   ]
}

A IA chamadora invoca a ferramenta de preset diretamente (ex.: code_review) e ainda escreve os seletores de models, atribuindo qualquer modelo a qualquer papel do preset. Presets são obrigatórios: cada seletor deve usar um papel do preset e cada papel deve ser preenchido dentro de sua cardinalidade, caso contrário o resultado é um erro dizendo o que corrigir.

Chaves:

ChaveTipoPadrãoDescrição
descriptionstring | string[]obrigatórioDescrição MCP para a ferramenta de preset.
rolesPresetRole[]obrigatórioPapéis aceitos pelo preset.
mode"sequential" | "parallel" | "private" | "independent""sequential"Controla quais turnos anteriores cada orador da rodada vê.
defaultRoundsinteiro positivo1Rodadas usadas quando a chamada omite rounds.
synthesizerstringnenhumPapel que produz turnos de síntese. Um papel de assento único é neutro e fica de fora das rodadas; um papel de múltiplos assentos (max > 1 ou null) participa, e seu primeiro assento ativo sintetiza.
synthesizeEvery"end" | non-negative integer"end"Executa a síntese no final ou a cada N rodadas.
eliminatorstringnenhumPapel neutro que emite eliminações. Obrigatório com eliminateEvery.
framerstringnenhumPapel neutro que abre e redireciona a discussão.
reframeEvery"end" | non-negative integer"end"Reformula apenas na abertura ou a cada N rodadas após a abertura.
closingStatementsbooleanfalseExecuta uma fase de encerramento antes da síntese final.
eliminateEveryinteiro não negativo | "spread"0Permite que o eliminador remova um orador a cada N rodadas, ou distribua os cortes uniformemente. Apenas preset.
eliminationsOptionalbooleanfalsePermite que o eliminador recuse uma eliminação. Apenas preset.
enterEveryinteiro não negativo0Começa com um orador por equipe e adiciona um orador reserva a cada N rodadas. Apenas preset.
votestringnenhumInstruções de votação; habilita votação anônima.
voteEvery"end" | non-negative integer"end"Vota no final ou a cada N rodadas.
voteVisibility"aggregate" | "ballots""aggregate"Inclui apenas totais ou também escolhas de votação anonimizadas no transcript.
allowSelfVotebooleantrueInclui o próprio assento de cada votante em seu menu de candidatos.
voteByTeambooleanfalseApresenta uma escolha por @team e agrega votos por equipe.

Chaves do objeto de papel:

ChaveTipoPadrãoDescrição
rolestringobrigatórioNome do papel usado nos seletores model:role.
descriptionstring | string[]arquivo de papel correspondenteInstruções inline; arrays são unidos com quebras de linha. Caso contrário, config/roles/<role>.md deve existir.
mininteiro não negativo1Mínimo de falantes; 0 torna o papel opcional.
maxinteiro positivo ou null1Máximo de falantes; null é ilimitado.
maxTokensinteiro positivo8192Limite de saída para este papel quando o chamador omite maxTokens. Um valor explícito do chamador substitui todos os papéis, incluindo papéis neutros. Inclui raciocínio oculto.
silentbooleanfalsePermite que o papel observe e vote sem falar nas rodadas normais.
voterbooleantodos os papéis elegíveisRestringe cédulas anônimas a papéis marcados quando qualquer papel está marcado.
candidatebooleantodos os papéis elegíveisRestringe opções de cédula a papéis marcados quando qualquer papel está marcado.
closingbooleantodos os papéis elegíveisRestringe encerramentos a papéis marcados; apenas o primeiro falante marcado por equipe ou papel sem equipe encerra.
closingLastbooleanfalseExecuta este encerrador após encerramentos paralelos com suas declarações em contexto; requer closing: true.
tagTeambooleanfalseRotaciona um falante marcado por @team em cada rodada normal. Não pode ser combinado com enterEvery.
cameobooleanfalseFala em exatamente uma rodada — o cameoRound da chamada, senão o ponto médio — em vez de todas as rodadas. Um intruso, não um regular.

Por exemplo, uma chamada de tribunal atribui advogados a lados com tags @team. O primeiro advogado listado para cada lado faz a declaração de encerramento daquele lado:

{
   "models": [
      "gpt:lawyer@prosecution",
      "grok:lawyer@prosecution",
      "claude:lawyer@defense",
      "kimi:juror",
      "llama:juror",
      "mistral:juror",
      "opus:judge"
   ],
   "objectives": {
      "prosecution": "Prove liability.",
      "defense": "Defeat liability."
   }
}

Notas de autoria

Coisas que complicam ao escrever um preset:

  • Eliminações usam rótulos exatos. O juiz copia um comando CUT <label> do menu elegível e depois dá um motivo na linha seguinte. Cortes opcionais também permitem KEEP ALL. Números puros e seleções ambíguas são rejeitados. Substituições locais do prompt de eliminação devem usar este formato de comando.
  • Eliminação obrigatória pode travar. Tentativas limitadas de limpeza não garantem um único sobrevivente. Uma execução não resolvida mantém seu resumo e discussão, mas retorna isError: true e structuredContent.incomplete com os rótulos restantes. Concursos com eliminação opcional não estão sujeitos a este requisito.
  • min/max contam cada falante naquele papel, não por equipe. Eles limitam o papel inteiro em todas as equipes, então um papel de tag team wrestler que precisa cobrir lados de um 2v2 até um 6v6 é min: 4, max: 12 — um 3v3 é apenas uma escalação válida (6 lutadores) dentro desse intervalo, não seu próprio max: 3. O motor não pode impor "lados pares" ou "um por lado" — diga isso no description em vez disso.
  • Papéis neutros não podem ser em equipe. O eliminator, framer e um synthesizer de assento único rejeitam uma tag @team, então um papel que pertence a um lado não pode ocupar essas funções.
  • Algumas chaves exigem outras, e uma violação é detectada no carregamento: synthesizeEvery precisa de um synthesizer, eliminateEvery precisa de um eliminator, reframeEvery precisa de um framer, closingLast precisa de closing, opções de votação precisam de vote e um sintetizador de assento único precisa de pelo menos um outro papel obrigatório (ele fica de fora das rodadas normais). enterEvery e tagTeam não podem ser combinados — ambos decidem quem fala.
  • Um preset malformado é ignorado, não fatal. Ele registra ❌ preset skipped — Invalid <file>: <reason> e todas as outras ferramentas ainda são registradas, então verifique o log do servidor quando uma ferramenta de preset não aparecer.
  • Custo é speakers × rounds chamadas de modelo em série, então defaultRounds e um max generoso multiplicam rapidamente. mode: "parallel" colapsa cada rodada para seu falante mais lento, ao preço de os falantes não se verem mais dentro de uma rodada.

Este repositório inclui estes presets — edite ou exclua livremente:

battle_royale
Concurso de todos contra todos; um supervisor coroa um vencedor.
brainstorm
Geração divergente de ideias entre modelos.
bullying
Um modelo defende uma posição enquanto os demais se unem contra ele; um professor opcional decide se ele se manteve firme.
code_review
Revisão de código estruturada com múltiplos modelos.
courtroom
Advogados com tags de equipe argumentam lados opostos, jurados votam por lado e um juiz decide.
debate
Lados opostos argumentam uma questão para uma síntese.
double_blind
Painel cego independente — ninguém vê os outros.
election
Candidatos fazem campanha, então o grupo decide por voto secreto — o voto anônimo é o veredito, não a decisão de um juiz. Um incumbent opcional defende um histórico; um electorate silencioso opcional lê todas as rodadas e vota sem fazer campanha.
final_girl
Sobreviventes eliminados um por rodada até restar um.
focus_group
Painel moderado que reage às ideias uns dos outros.
gauntlet
Corrida privada de auto-refinamento entre rodadas.
quick_take
Verificação rápida de intuição com duas vozes — uma resposta direta, uma linha de contestação.
refine
Polimento em revezamento de um artefato existente.
tag_team
Luta de tag team: um lutador com tag @team é elegível por equipe a cada rodada e os demais esperam no córner, rotacionando a cada rodada para que um lado de três pessoas passe por todos os três. Um run_in opcional entra no ringue por uma única rodada (agende com cameoRound), um announcer opcional narra a luta e o ref nomeia a equipe vencedora.
tiebreak
Uma terceira voz decisiva resolve um impasse.
war_games
Uma luta em gaiola com entrada escalonada: combatentes com tag @team entram um de cada vez enquanto um árbitro neutro marca faltas e nomeia a equipe vencedora, com um booker opcional que define a luta.
workshop
Equipe criativa diferenciada.

Quais presets empacotados são registrados como ferramentas é controlado por PRESETS — sem definir, registra todos, e um preset apenas local (um slug sem contraparte empacotada) que você adiciona sob seu próprio config/presets/ é sempre registrado independentemente, já que criar um é o opt-in. Um arquivo local que reutiliza um slug empacotado é uma personalização daquele preset, não um novo, então ainda obedece a PRESETS. Sem presets empacotados e sem presets locais, não há ferramentas de preset — mas note que o conjunto empacotado é a camada base, então você recebe todos os presets empacotados mesmo sem enviar uma pasta config/presets/ própria.

O texto do papel orienta a saída, não a limita — use maxTokens para um limite rígido por turno e orce generosamente para modelos de raciocínio e quóruns de múltiplas rodadas. tokenBudget é um eixo diferente: conta a entrada e a saída de cada chamada para toda a execução, então dimensione a partir de turns × (context + maxTokens) e defina acima da estimativa — a entrada domina quando o transcripto cresce.

Orientação de IA — config/description.md

Markdown opcional servido aos clientes como MCP instructions — descreva seus modelos e quando a IA deve usar cada um. Veja a cópia deste repositório para um modelo. O marcador literal {longRuns} é substituído por config/sync/long-runs.md ou config/async/long-runs.md dependendo de ASYNC_TOOLS, então a orientação de entrega (bloquear-e-esperar vs iniciar/consultar/cancelar) acompanha o modo sem duplicar o resto do arquivo. Um description.md local sem o marcador é servido como escrito; um aviso de inicialização observa que a orientação de modo está ausente.

Texto de ferramenta, campo e mensagem — config/*.json e config/tools/*.md

O texto que um chamador lê para dirigir o Legion — descrições de ferramentas e campos, andaimes de prompt, erros de execução — vive na configuração, não no código. (Descrições de formato de resultado em outputSchema, linhas de escalação geradas e erros de inicialização permanecem no código.) Cada arquivo mescla sobre a base JSON empacotada por chave, então substitua apenas o que quiser; abra as cópias enviadas para ver o conjunto completo de chaves e espaços reservados {token}:

  • config/tools/quorum.md, poll.md, cancel.md — descrições para a ferramenta quorum e (somente modo assíncrono) as ferramentas poll / cancel. Exclua uma para voltar à sua string embutida. Ferramentas de modelo se descrevem a partir do description de seu arquivo de modelo, e ferramentas de preset a partir do próprio preset.
  • config/sync/long-runs.md, config/async/long-runs.md — o bloco específico do modo inserido em description.md em {longRuns}.
  • config/schema.json — descrições de campos de entrada (prompt = campos compartilhados, quorum = somente quórum; uma chave quorum vence em conflito de nome).
  • config/prompts.json — os modelos de formatação de prompt que os modelos leem: contrato de papel, bloco de contexto, cabeçalho de transcripto, banners de rodada. Ajuste aqui o quão fortemente os papéis se vinculam e como as rodadas são enquadradas.
  • config/errors.json — mensagens de erro de execução mostradas à IA chamadora.

(Erros de inicialização/validação de configuração permanecem no código — uma mensagem que relata um arquivo de configuração quebrado não pode viver dentro dele.)

Variáveis de ambiente

VariávelObrigatórioDescrição
DEFAULT_BASE_URLnão*Raiz da API para modelos sem um baseUrl — o SDK acrescenta /responses. Ex.: https://api.openai.com/v1, https://<res>.openai.azure.com/openai/v1; um proxy LiteLLM funciona na sua raiz simples.
DEFAULT_API_KEYnão*Chave de API para modelos sem um apiKey. Permanece no lado do servidor.
ALLOW_NO_MODELSnãotrue inicia mesmo quando nenhum arquivo de modelo existe: zero ferramentas de modelo; quorum e ferramentas predefinidas são registradas, mas falham ao serem usadas até que um config/models/*.json apareça (recarregado a quente por solicitação). Para demonstrações e sandboxes de registro que apenas listam ferramentas. Padrão false — modelos ausentes permanecem fatais.
MCP_TRANSPORTnãohttp (padrão) ou stdio.
HOSTnãoEndereço de bind HTTP (padrão 127.0.0.1). Defina 0.0.0.0 para expor — então defina ALLOWED_HOSTS.
ALLOWED_HOSTSnãoNomes de host separados por vírgula para proteção contra rebinding de DNS em binds não localhost.
PORTnãoPorta HTTP (padrão 5000; ignorada por stdio).
MAX_ROUNDSnãoMáximo de rodadas de discussão que a ferramenta quorum aceita (padrão 5).
MAX_TOKENSnãoMáximo visível de maxTokens por turno que um chamador pode solicitar; valores maiores são rejeitados (padrão 3000). Limites de função predefinidos acima disso são limitados. Alocações de raciocínio por modelo são adicionadas por cima.
MODEL_TIMEOUTnãoTempo limite por chamada de modelo em ms (padrão 90000), para que um assento travado não trave um conselho. Repetido uma vez, então o pior caso de um assento é aproximadamente o dobro antes de se tornar um turno falho.
TOKEN_BUDGETnãoOrçamento padrão suave cumulativo de tokens para uma execução de quorum (não definido = sem limite; tokenBudget por chamada substitui).
DYNAMIC_ROLESnãoPermitir que a IA chamadora defina funções quorum ad-hoc inline (padrão true).
PRESETSnãoLista de permissões separada por vírgulas de slugs predefinidos empacotados para registrar como ferramentas (ex.: code_review,debate). Não definido = todos os predefinidos empacotados, então atualizações nunca descartam um silenciosamente; definido = apenas estes, então um predefinido empacotado recém-lançado nunca aparece sem convite. Predefinidos que você adiciona sob seu próprio config/presets/ são sempre registrados — criar um é o opt-in — enquanto um arquivo local que compartilha um slug predefinido personaliza esse predefinido e ainda obedece à lista. Nomes desconhecidos são ignorados.
LOG_LEVELnãodebug | info | warn | error (padrão info).
ASYNC_TOOLSnãotrue executa conselhos em segundo plano: quorum e ferramentas predefinidas retornam um identificador de trabalho imediatamente e ferramentas poll / cancel são expostas. Para hosts cujas chamadas de ferramenta expiram antes que um conselho termine. Os trabalhos vivem apenas na memória deste processo. Padrão false.
TRUST_PROXY_AUTHnãotrue vincula cada trabalho ao chamador nomeado pelos cabeçalhos X-MS-CLIENT-PRINCIPAL-* (Azure Container Apps Easy Auth). Somente seguro atrás de um ingress que remove cópias fornecidas pelo cliente. Padrão false — qualquer detentor de um jobId pode consultar ou cancelá-lo.
JOB_MAX_ACTIVE / JOB_MAX_RETAINED / JOB_RETAIN_MS / JOB_POLL_INTERVAL_MS / JOB_SHUTDOWN_GRACE_MSnãoLimites de trabalho assíncrono: conselhos simultâneos (3), trabalhos concluídos mantidos (20), retenção (1800000 ms), cadência de consulta sugerida (10000 ms), dreno SIGTERM (20000 ms).

* Todo modelo deve resolver um baseUrl e apiKey de seu arquivo ou dos padrões — validado na inicialização.

O servidor falha rapidamente na inicialização em um diretório de modelos ausente/vazio (a menos que ALLOW_NO_MODELS=true), arquivos de modelo inválidos, um endpoint ou chave irresolvível, ou dois nomes de arquivo que se transformam no mesmo slug de ferramenta.

Roteamento

Toda chamada de ferramenta é uma solicitação stateless de API Responses de uso único. Modelos cujos endpoints falam nativamente Responses (OpenAI, Azure OpenAI / Foundry) definem um baseUrl para serem chamados diretamente; o restante recorre aos padrões — tipicamente um gateway compatível com OpenAI como LiteLLM que faz a ponte para suas APIs nativas.

Registro

  • info (azul): início do servidor e uma linha de metadados por chamada de modelo — modelo, latência, uso de tokens, função, presença de contexto. Sem conteúdo de prompt/resposta.
  • debug (cinza): adicionalmente registra o prompt e a resposta completos (o contexto é anotado como presente, não impresso).
  • warn (laranja) / error (vermelho): fallbacks e falhas.

A cor é desativada automaticamente quando stderr não é um TTY.

Executar

Um único ponto de entrada; o transporte vem de MCP_TRANSPORT (http é o padrão, defina stdio para clientes MCP de desktop).

Do npm (bin legion-mcp — execute a partir de um diretório contendo seu config/):

npx legion-mcp                             # Streamable HTTP transport on :$PORT/mcp
$env:MCP_TRANSPORT='stdio'; npx legion-mcp # stdio transport

Instalado globalmente ou como dependência, o mesmo binário está em PATH:

npm install -g legion-mcp
legion-mcp

Desenvolvimento (sem etapa de build, via tsx):

npm run dev         # Streamable HTTP transport on :$PORT/mcp
npm run dev:stdio   # stdio transport

Produção (compilado para bin/server.js):

npm run build
npm start           # http
npm run start:stdio # stdio

Modo assíncrono — ASYNC_TOOLS=true

Um conselho pode levar minutos; ChatGPT e Codex hospedados abandonam uma chamada de ferramenta muito antes disso (observado ~124 s; Codex usa padrão de 60 s). Com ASYNC_TOOLS=true, as ferramentas quorum e predefinidas validam a solicitação, iniciam o conselho em segundo plano e retornam imediatamente com um identificador — structuredContent.jobId, o state/phase atual e um pollIntervalMs sugerido. Duas ferramentas extras aparecem:

  • poll — somente leitura; retorna o estado do trabalho, as respostas e notas públicas concluídas até agora e (uma vez terminal) result: a saída completa do conselho em seu formato usual. full: true adiciona a transcrição renderizada e a linha do tempo.
  • cancel — aborta chamadas de modelo em andamento, retorna o que já foi concluído e encerra o trabalho como cancelled sem síntese. Idempotente.

Ferramentas de modelo individuais permanecem síncronas. Ferramentas de servidor instructions mudam para o fluxo de trabalho iniciar/consultar/cancelar para que o modelo chamador saiba o que fazer. Os trabalhos são mantidos na memória do processo sob limites JOB_* — um reinício os perde, e uma solicitação roteada para uma réplica diferente não pode vê-los, então execute uma única instância (ou fixe roteamento sticky) quando este modo estiver ativo. Defina TRUST_PROXY_AUTH=true atrás de Azure Container Apps Easy Auth para vincular cada trabalho ao chamador autenticado; sem isso, a posse do jobId é o único controle de acesso.

Experimente

Liste as ferramentas com o MCP Inspector:

npx @modelcontextprotocol/inspector -e MCP_TRANSPORT=stdio npx tsx ts/server.ts

Uso no VS Code

Adicione ao seu mcp.json — do npm:

{
   "servers": {
      "legion": {
         "command": "npx",
         "args": ["-y", "legion-mcp"],
         "cwd": "path/to/your/config/parent",
         "env": {
            "MCP_TRANSPORT": "stdio",
            "DEFAULT_BASE_URL": "https://your-gateway.example.com",
            "DEFAULT_API_KEY": "sk-your-key"
         }
      }
   }
}

Ou de um clone:

{
   "servers": {
      "legion": {
         "command": "node",
         "args": ["bin/server.js"],
         "cwd": "path/to/legion",
         "env": {
            "MCP_TRANSPORT": "stdio",
            "DEFAULT_BASE_URL": "https://your-gateway.example.com",
            "DEFAULT_API_KEY": "sk-your-key"
         }
      }
   }
}

Para o transporte HTTP, aponte seu cliente para http://<host>:<PORT>/mcp.

Saúde

  • GET /health — liveness barato: confirma que o processo está ativo e a configuração carregada. Retorna { status: "ok", name, version, models } (uma contagem). Não faz chamadas externas. É isso que os HEALTHCHECKs de contêiner e as sondas de liveness/readiness do Kubernetes devem acessar.
  • GET /health?deep — verificação opcional de conectividade: envia um prompt minúsculo para cada modelo e relata a acessibilidade por modelo (503 se algum falhar). Faz uma chamada real e cobrável por modelo, então use manualmente — não conecte a uma sonda automática.

Implantar

Exemplos prontos de implantação de contêiner (Azure App Service, Azure Container Apps, Docker Compose, Kubernetes e Compose + Caddy para HTTPS) estão em examples/ — cada um instala Legion do npm e inclui um config/ completo de substituição.