The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Legion listing page.
"I am Legion, for we are many."
An MCP-native model council. Legion exposes LLMs as individual tools and orchestrates them into debates, juries, blind panels, private refinement gauntlets, workshops, and custom multi-model deliberations.
Every model is reached through the OpenAI Responses API wire format. Use OpenAI or Azure directly, route other providers through a compatible gateway (such as a LiteLLM proxy), and configure the entire council through hot-reloadable files.
Claude →
claude). Each accepts a prompt plus optional context, role, system,
temperature, and maxTokens.quorum tool fans one prompt out to several models — with roles,
multi-round discussion, visibility modes, and synthesis — and returns each
answer separately. See Presets for the
orchestration options.structuredContent, not the answer text.
Logging goes to stderr (safe for stdio).quorum tool covers the ad-hoc multi-model case, and each preset in
config/presets/ is exposed as its own enforced, pre-staffed council tool.store: false. Nothing is
persisted, so there is no database and no conversation state to manage.From npm — no clone, no build:
From a clone:
All configuration lives in a config/ directory. The bundled defaults are
always the base layer; a config/ folder in the current working directory
is overlaid on top of them, per file:
models/, roles/, presets/, tools/): a local
file overrides the bundled file of the same name; a local-only file is added;
every bundled file you don't touch stays. So dropping in one
config/presets/refine.json overrides just that preset — the other bundled
presets remain.prompts.json, errors.json, schema.json): merged
per key — bundled < local. A partial local file overrides only
the keys it sets.description.md: local wins whole if present, else bundled.The overlay can override or add, but not delete a bundled entry. To turn off
bundled presets you don't want, use DISABLE_PRESETS (see below).
Installing from npm? You must supply your own model files. The bundled config ships only key-free
*.example.jsonmodel files, which the scanner deliberately ignores — so the bundle contributes zero real models. With no real model file the server fails fast at startup (No model files found in ...). Drop oneconfig/models/<name>.jsonnext to where you run the server (see below) — the rest falls back to the bundled defaults.
The layout below is identical either way, and everything hot-reloads per request.
config/models/*.jsonAt least one model file is required — the server fails fast without one.
Each JSON file becomes a tool, named after the slugified file name
(config/models/fable.json → tool fable):
model (required) — the deployed model id the endpoint routes to.description — helps the calling AI pick the right model.system — optional baseline system instructions baked into every call to
this model.baseUrl / apiKey — optional; omitted values fall back to
DEFAULT_BASE_URL / DEFAULT_API_KEY.omitParams — optional list of request params to drop for this model, e.g.
["temperature"]. The server stays provider-agnostic: it never assumes which
models reject which params — you declare each model's quirks here. Useful for
reasoning models and some deployments that reject temperature.Hot-drop: the directory is re-scanned per request — add or edit a model file and it's live on the next call, no restart.
Secrets & git: model files can contain API keys, so config/models/*.json
is git-ignored. Copy a *.example.json (tracked, key-free, ignored by the
scanner) to get started:
config/roles/*.mdOptional hot-droppable instruction files. Each .md file becomes a named role
(slugified from filename). Drop a file, it's live on the next call. This repo
ships skeptic.md, builder.md, judge.md, and short.md (a terse "answer
immediately, no deliberation" role useful for constrained-output turns) as
ready-to-use starters — edit or delete them freely (they hold no secrets).
Available selectors in tools become roleName, e.g. passing role: "skeptic"
or using "model:skeptic" in quorum.models.
config/presets/*.jsonOptional hot-droppable council recipes, one JSON file per preset (named
after the slugified file name, like models). Each preset becomes its own
tool — drop config/presets/code_review.json and a code_review tool appears
on the next request. Each preset has a description, a roles list, and
optional authoritative mode / synthesizer defaults. Each role defines its
behavior inline — a role's description is its instructions (the behavior
contract); a role with no description falls back to a matching
config/roles/<role>.md file:
The calling AI invokes the preset tool directly (e.g. code_review) and still
writes the models selectors, assigning any model to any preset role. Presets
are enforced: every selector must use a preset role and every role must be
staffed within its cardinality, else the result is an error saying what to fix.
Keys:
| Key | Type | Default | Description |
|---|---|---|---|
description | string | string[] | required | MCP description for the preset tool. |
roles | PresetRole[] | required | Roles accepted by the preset. |
mode | "sequential" | "parallel" | "private" | "independent" | "sequential" | Controls which prior turns each round speaker sees. |
defaultRounds | positive integer | 1 | Rounds used when the call omits rounds. |
synthesizer | string | none | Neutral role that produces synthesis turns. |
synthesizeEvery | "end" | non-negative integer | "end" | Runs synthesis at the end or every Nth round. |
framer | string | none | Neutral role that opens and redirects the discussion. |
reframeEvery | "end" | non-negative integer | "end" | Reframes only at opening or every Nth round after opening. |
closingStatements | boolean | false | Runs a closing phase before final synthesis. |
eliminateEvery | non-negative integer | 0 | Lets the synthesizer remove one speaker every Nth round. Preset-only. |
eliminationsOptional | boolean | false | Lets the synthesizer decline an elimination. Preset-only. |
enterEvery | non-negative integer | 0 | Starts one speaker per team, then adds one benched speaker every Nth round. Preset-only. |
vote | string | none | Ballot instructions; enables anonymous voting. |
voteEvery | "end" | non-negative integer | "end" | Votes at the end or every Nth round. |
voteVisibility | "aggregate" | "ballots" | "aggregate" | Includes only totals or also anonymized ballot choices in the transcript. |
allowSelfVote | boolean | true | Includes each voter's own seat in its candidate menu. |
voteByTeam | boolean | false | Presents one choice per @team and aggregates votes by team. |
Role object keys:
| Key | Type | Default | Description |
|---|---|---|---|
role | string | required | Role name used in model:role selectors. |
description | string | string[] | matching role file | Inline instructions; arrays are joined with newlines. Otherwise config/roles/<role>.md must exist. |
min | non-negative integer | 1 | Minimum speakers; 0 makes the role optional. |
max | positive integer or null | 1 | Maximum speakers; null is unbounded. |
silent | boolean | false | Lets the role observe and vote without speaking in normal rounds. |
voter | boolean | all eligible roles | Restricts anonymous ballots to marked roles when any role is marked. |
candidate | boolean | all eligible roles | Restricts ballot choices to marked roles when any role is marked. |
closing | boolean | all eligible roles | Restricts closings to marked roles; only the first marked speaker per team or unteamed role closes. |
closingLast | boolean | false | Runs this closer after parallel closings with their statements in context; requires closing: true. |
tagTeam | boolean | false | Rotates one marked speaker per @team into each normal round. Cannot combine with enterEvery. |
For example, a courtroom call assigns lawyers to sides with @team tags. The
first lawyer listed for each side gives that side's closing statement:
This repo ships these presets — edit or delete freely:
code_reviewdebatebrainstormquick_taketiebreakbattle_royalecourtroomelectionincumbent defends a record; an optional silent electorate reads every round and votes without campaigning.double_blindgauntletrefineworkshopfocus_groupfinal_girlwar_games@team-tagged combatants enter one at a time while a neutral ref calls fouls and names the winning team, with an optional booker who sets the match.Empty/missing folder → no preset tools.
Role text nudges output, it doesn't cap it — use
maxTokensfor a hard limit, and budget generously for reasoning models and multi-round quorums.
config/description.mdOptional markdown served to clients as MCP instructions — describe your
models and when the AI should use each. See this repo's copy for a template.
config/*.json and config/tools/*.mdAll user-facing text lives in config, not code, and hot-reloads per request.
Each file merges over the bundled JSON base per key, so override only what you want;
open the shipped copies to see the full key set and {token} placeholders:
config/tools/<tool>.md — a tool's description (e.g. quorum.md). Delete to
fall back to the built-in string.config/schema.json — input-field descriptions (prompt = shared fields,
quorum = quorum-only; a quorum key wins on a name clash).config/prompts.json — the prompt-shaping templates models read: role
contract, context block, transcript header, round banners. Tune how strongly
roles bind and how rounds are framed here.config/errors.json — runtime error messages shown to the calling AI.(Startup/config-validation errors stay in code — a message that reports a broken config file can't live inside it.)
| Variable | Required | Description |
|---|---|---|
DEFAULT_BASE_URL | no* | API root for models without a baseUrl — the SDK appends /responses. E.g. https://api.openai.com/v1, https://<res>.openai.azure.com/openai/v1; a LiteLLM proxy works at its plain root. |
DEFAULT_API_KEY | no* | API key for models without an apiKey. Stays server-side. |
ALLOW_NO_MODELS | no | true boots even when no model files exist: zero model tools; quorum and preset tools register but fail on use until a config/models/*.json appears (hot-reloaded per request). For demos and registry sandboxes that only list tools. Default false — missing models stay fatal. |
MCP_TRANSPORT | no | http (default) or stdio. |
HOST | no | HTTP bind address (default 127.0.0.1). Set 0.0.0.0 to expose — then set ALLOWED_HOSTS. |
ALLOWED_HOSTS | no | Comma-separated hostnames for DNS-rebinding protection on non-localhost binds. |
PORT | no | HTTP port (default 5000; ignored by stdio). |
MAX_ROUNDS | no | Max discussion rounds the quorum tool accepts (default 5). |
TOKEN_BUDGET | no | Default soft cumulative token budget for a quorum run (unset = no limit; per-call tokenBudget overrides). |
DYNAMIC_ROLES | no | Allow the calling AI to define ad-hoc quorum roles inline (default true). |
DISABLE_PRESETS | no | Comma-separated preset slugs to not register as tools (e.g. battle_royale,courtroom). Applies to bundled and local presets alike; unknown names are ignored. Unset = all presets registered. |
LOG_LEVEL | no | debug | info | warn | error (default info). |
* Every model must resolve a baseUrl and apiKey from its file or the
defaults — validated at startup.
The server fails fast at startup on a missing/empty models directory
(unless ALLOW_NO_MODELS=true), invalid model files, an unresolvable endpoint
or key, or two file names that slugify to the same tool.
Every tool call is a stateless, one-shot Responses API request. Models whose
endpoints natively speak Responses (OpenAI, Azure OpenAI / Foundry) set a
baseUrl to be called directly; the rest fall back to the defaults —
typically an OpenAI-compatible gateway like LiteLLM that bridges to their native
APIs.
info (blue): server start and one metadata line per model call — model,
latency, token usage, role, context presence. No prompt/response content.debug (gray): additionally logs the full prompt and response (context is
noted as present, not printed).warn (orange) / error (red): fallbacks and failures.Color is auto-disabled when stderr is not a TTY.
One entrypoint; the transport comes from MCP_TRANSPORT (http is the
default, set stdio for desktop MCP clients).
From npm (legion-mcp bin — run from a directory holding your config/):
Installed globally or as a dependency, the same binary is on PATH:
Development (no build step, via tsx):
Production (compiled to bin/server.js):
List the tools with the MCP Inspector:
Add to your mcp.json — from npm:
Or from a clone:
For the HTTP transport, point your client at http://<host>:<PORT>/mcp.
GET /health — cheap liveness: confirms the process is up and config
loaded. Returns { status: "ok", name, version, models } (a count). Makes no
external calls. This is what container HEALTHCHECKs and Kubernetes
liveness/readiness probes should hit.GET /health?deep — optional connectivity check: sends a tiny prompt to
every model and reports per-model reachability (503 if any fail). Makes a
real billable call per model, so use it manually — don't wire it to an
automatic probe.Ready-to-use container deployment examples (Azure App Service, Azure Container
Apps, Docker Compose, Kubernetes, and Compose + Caddy for HTTPS) live in
examples/ — each installs Legion from npm and ships a complete
drop-in config/.