Model Council
Allows OpenAI-compatible endpoints to be seated as council members, letting the assistant send them prompts, materials, and discussion rounds through the OpenAI-compatible API.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Model CouncilAsk the council to peer-review this argument: AI will never be conscious."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Model Council
English | 简体中文
An MCP server that seats other LLMs at your table. Your assistant asks them, reads their answers as tool results, relays those answers back and forth for critique, and gives you one merged conclusion — inside a single normal conversation, with no copy-paste.
Your assistant chairs the council. Any number of members, from any mix of OpenAI-compatible and Anthropic-compatible endpoints — a hosted API, a self-run gateway, a local server, or several of each.
Tools
Tool | What it does |
| Ask one member by id |
| Ask everyone (or a named subset) the same prompt in parallel, answers side by side. |
| Run one more round yourself: show the members everything said last round — including answers only you can produce — and get them back revised |
| The prompt to hand your own seat for the next round, word-for-word what the members got. No network calls |
| The roster: ids, endpoints, weights, what each member can be shown, the route each takes out, call budget, and whether it is ready. No network calls |
| Ask a provider's |
Members are stateless and cannot see your conversation, so the chair passes
everything they need in each call. That is exactly what makes cross-review work:
it puts one member's answer inside another's prompt.
Giving the council something to read
A question is usually about something — a spec, a log, a diff, a screenshot. The
obvious way to include it is to paste it into prompt, and that is the expensive
one, in the place nobody watches: prompt is an argument the chair writes, so
a long document costs a full copy of itself in generated tokens on every call,
and what reaches the members is whatever the chair managed to reproduce. For
forty pages that is not reliably the document. A council reviewing a paraphrase
is not reviewing the thing, and nothing in the transcript would say so.
materials names it instead:
ask_all(
prompt="Where would this break under load?",
materials=[
{"label": "The design", "path": "/abs/path/design.md"},
{"label": "Latency graph", "path": "/abs/path/p99.png"},
{"label": "My summary", "text": "…something with no file behind it…"},
],
rounds=2,
)The server reads each one and puts it ahead of the question, in the same place for every member and every round. The position is not presentation:
Identical bytes. Every member argues with the same copy, which is what makes their disagreement about the document rather than about which version each was given.
A prefix that can be cached. Everything up to the end of the material is the same in every call of a discussion. The Anthropic format is told so explicitly, with one
cache_controlbreakpoint at that boundary; endpoints that cache by prefix on their own get the shape they need either way. Set"cache": falseon a member whose gateway rejects the field.The chair pays once. A path costs the chair a line, not a copy of the document, and
ask_allcarries the material into round 2 and 3 by itself.
Images are the case that matters most. Without them the chair has to describe
the screenshot in prose — and then every member reads the same description, so
anything the chair misread is misread by the whole council at once, and
cross-review cannot recover it. It is the one input where passing it badly
quietly removes the independence the council is for. Png, jpeg, gif and webp go
to every member whose model can see; a member that cannot should be configured
"vision": false, and it then sits out the calls that carry an image rather than
answering about text alone as though it had seen the picture. The transcript
names who sat out.
How to tell which members those are: a member that cannot see rarely says so.
One Anthropic-compatible gateway tested while building this accepted the image
blocks, discarded them, and the model then named a colour — not "I was sent no
image", which the prompt had explicitly asked for in that case. Text material
through the same endpoint arrived intact, so nothing about the call looked wrong.
Show a member one unambiguous picture and ask what is in it; a confidently wrong
answer is the symptom, and "vision": false is the fix.
Two limits worth knowing. A material that cannot be read stops the call — a
council asked about a document it never received will answer anyway, fluently,
and read exactly like one that had. And revision_prompt names your files
rather than pasting them back, at the position the members were given them, so
open them for your own seat before you hand it the rest.
Whether this server will read a path at all depends on how it was started; over
HTTP it will not, unless its operator named a directory.
list_council says which, in a line above the table.
Rounds
ask_all(prompt, rounds=2) runs that cross-review for you. Round 1 is the usual
parallel ask. Round 2 goes back to each member carrying the question plus every
answer from round 1 — its own and the others', verbatim — and asks it to revise:
take what is right, correct what is not, and say where it still disagrees and
why. The transcript comes back round by round, so you can see who moved and who
held their ground.
Carrying the previous round back is the whole mechanism. Members remember nothing between calls, so without it a second round is just the same question asked twice. Up to 3 rounds; each one costs another call per member and a longer prompt than the last, so 1 is right for a survey of opinion and 2 for a question where the disagreement is the interesting part.
rounds is chosen before anything has been asked, which is the one thing
wrong with it: you commit to a second round without having read the first, and a
council that turns out to agree costs exactly what one still arguing would. Ask
with rounds=1 when you would rather look first, then buy the next round with
revise — it runs exactly one more, as many
times as you judge it worth, and has no ceiling of its own. Reading before you
buy is usually the cheaper side: another round is a full call per member, while
revise costs you only the answers written back into it. The 3 on ask_all is a
ceiling on what one call will spend, not on what the discussion can have, and the
transcript says so when you reach it.
Members can also carry different weights, for the common case where the council is not a council of equals.
The standing objection
A council mostly agrees, and its agreement is the least informative thing it produces. Everything above works against convergence — the members are anonymous to each other, they are never told the weights, and the closing instruction tells them not to cave — but none of it creates any pressure to diverge. The strongest objection to a plan is not volunteered by members who think the plan is fine.
steelman seats one:
ask_all(prompt, rounds=3, steelman={})One member writes the strongest case against whatever the table has converged on, each round, and it goes back to everyone as an ordinary anonymous answer. In the next round they have to deal with it.
No member is told to argue a side it does not hold. That is the line, and it is what separates this from a debate mode. What the members say is still what they think; the assignment lives in one extra call they are never told about. Handed a seat marked "arguing against", a model discounts the argument instead of answering it — so the provenance goes to you, in a note at the end of the transcript, and the argument goes to them.
It speaks every round, not once. This is the part that looks like a detail
and is not. An objection that cannot reply to its own rebuttal is quoted rather
than represented: it cannot correct a misreading of itself, so by the third round
the table is arguing with its paraphrase and calling that an answer. tenure
buys fewer rounds than the default, and when the seat is retired early the
transcript says it was retired by configuration — because an unexplained silence
reads exactly like a position abandoned.
The mirror of that is worth as much: the last thing the objection says is the
last thing in the transcript, so nobody has been asked to take it on, and the
transcript says that too. An objection nobody answered is not a point that
stood — it is a point that was never examined. With rounds=2 that is true of
everything it said, which is why the note names the setting that fixes it rather
than leaving you to notice after paying.
Read what it produces as the strongest objection this council can make to order, never as evidence that anyone holds it. A point of its that survives being answered is worth something; the same point in the round it appeared is worth nothing yet.
By default the first member being asked writes it, and also answers as itself —
the two calls are unrelated as far as it knows. Name another with
steelman={"model": "glm"}.
Seating an answer you already have
Your assistant is not only the chair — it can answer too, and in Claude Code or
Codex it can spawn a subagent to answer as well. Those answers used to sit
beside the council's, compared by hand at the end. guests puts them in it:
ask_all(
prompt="What are the traps in this plan?",
guests=[{"label": "Subagent", "text": "<what your subagent answered>"}],
rounds=2,
)Round 1 prints it beside the members. From round 2 every member is handed the text verbatim and asked to argue with it:
--- ANSWER C (does not revise between rounds) ---
the migration has no rollback pathThat is the whole difference. Without it the models never learn your subagent had an opinion, and you are left doing the comparison yourself — which is the work the rounds mechanism already does better, because it lets the models answer each other rather than answering into a void.
Notes worth having:
Pass the text verbatim, not a summary. The members are shown exactly what you pass, and a summary is not the thing you wanted critiqued.
A guest speaks once. There is nobody to ask it for a revision, so it does not reappear after round 1. The transcript says so, because a seat that vanishes otherwise reads as a position abandoned.
A guest counts as a voice. One member plus one guest is enough for a real second round — your subagent against one model is still a discussion.
weightworks on guests too, on the same scale as the members'.Guests are per call. Nothing is configured, and nothing persists.
A subagent as a full member
A guest speaks once. To let a voice you produce yourself keep up with the
members — answer, read the others, revise, round after round, exactly as they do
— you have to drive the rounds, because this server cannot spawn your subagent.
revise runs one round on demand:
ask_all(prompt) → members answer round 1
...you spawn your subagent on the same prompt → its round-1 answer
revise(prompt, round=1, answers=[
{"model": "glm", "text": "<glm's round-1 answer>"},
{"model": "sol", "text": "<sol's round-1 answer>"},
{"label": "Subagent", "text": "<subagent's round-1 answer>"},
]) → members come back revised
...you re-run your subagent on the same material
revise(prompt, round=2, answers=[...round 2...]) → and so onNaming the member with model is what makes it a revision: that member is handed
its own previous answer back as its own, and asked to move from it rather than
answer fresh. An entry with label instead is a voice from outside the roster.
The members cannot tell the difference. An outsider passed to revise is
presented exactly as another member is — a bare letter, --- ROUND-1 ANSWER C ---
— because it will answer again next round, and it is the same reason an ask_all
guest is flagged as finished. Describing a voice as done when it is about to
speak again misrepresents the discussion to the models doing the arguing.
Prompt your subagent with revision_prompt, not with the original question.
This is the one way to get the loop wrong, and it fails silently: a subagent
handed the question again simply reproduces its previous answer, the transcript
still looks like a discussion, and nothing marks the round where that seat
stopped taking part.
revision_prompt(prompt, answers=[...same as revise...], seat="Subagent", round=1)It returns exactly what the members were given — its own previous answer marked
as its own, everyone else's, and the same closing instruction, including the line
telling it not to abandon a position it still believes just because it is
outnumbered. That sentence is most of what keeps a council from collapsing into
agreement, and a seat prompted without it is not being asked the same question as
the others. The call is local and makes no network requests, so run it alongside
revise rather than after it.
revise is unbounded — the 3-round ceiling on ask_all exists because that call
spends its own budget, while here every round is one you chose to pay for.
|
| |
Calls | one | one per round |
Your voice | speaks once | revises every round |
Rounds driven by | the server | you |
The members argue anonymously
Inside a revision round the other answers arrive as letters, never as names:
--- ROUND-1 ANSWER B ---
concurrent writes raceA model name is the same kind of signal as a weight, and the stronger of the two: models hold firm priors about each other's makers. Withholding the number while printing the brand would close the smaller channel and leave the larger one open. What reaches a member is the argument, which is the thing we wanted weighed.
Letters come from position at the table, so they are stable: B is the same seat
to every member and in every round, which is what lets one of them say "B was
wrong about the index" and still be understood next round.
You keep the names. Every transcript that contains a revision round ends with the key:
[the members saw each other as letters, not names: A = GLM-5.3, B = GPT-5.6-sol,
C = Subagent. ...]You are the exception because you are the one who has to tell them apart — see who moved, who held, and who was talking about whom.
One honest limit: this covers the labels the server writes, not a name a model puts inside its own answer. That prose is carried verbatim into the next round, because the alternative is editing the text the others are meant to be critiquing. Anonymity here removes the standing signal, not every mention.
Retries
A call that fails transiently is retried before it is reported: HTTP 429 and
5xx, and dropped or timed-out connections. Backoff is exponential from 1s with
jitter, and a Retry-After header wins over that curve — unless it asks for
longer than 30s, in which case the call stops and says so rather than sitting on
it. Failures that will not change on a second look — 401, 404, a malformed
response body — are reported immediately; retrying them only spends the same
quota to be told the same thing. Defaults to 2 retries; set retries: 0 for the
old single-shot behaviour.
A member that exhausts its attempts returns its error as that member's answer, so the rest of the council still answers — unless it has somewhere else to go, in which case it goes there. See Backups.
Related MCP server: conclave
Install
The server is listed on the official MCP Registry
as io.github.Totti0135/model-council, so a client that browses the registry can
find and add it there. To wire it up by hand instead, read on.
It runs from PyPI with no clone and no virtualenv. You need uv:
curl -LsSf https://astral.sh/uv/install.sh | shClaude Desktop
Easiest is the desktop extension. Download model-council-<version>.mcpb from
the latest release
and drag it onto Settings → Extensions. The app asks for the endpoints and keys
in a form and keeps the keys in your OS keychain, so no file on disk holds them.
The form seats two models; for a larger council, point its "Config file" field
at a JSON config (see below).
To wire it up by hand instead, edit claude_desktop_config.json
(Settings → Developer → Edit Config), add the block below, then fully quit and
reopen the app — Cmd-Q, not just closing the window. You will know it worked
when the tools menu lists model-council.
{
"mcpServers": {
"model-council": {
"command": "uvx",
"args": ["model-council-mcp"],
"env": {
"COUNCIL_MODELS": "gpt5,glm",
"GPT5_BASE_URL": "https://your-openai-compatible-host/v1",
"GPT5_API_KEY": "sk-xxxxxxxx",
"GPT5_MODEL": "gpt-5",
"GLM_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
"GLM_API_KEY": "xxxxxxxx",
"GLM_MODEL": "glm-4.6",
"GLM_FORMAT": "anthropic"
}
}
}
}Claude Code
claude mcp add model-council -e GPT5_BASE_URL=... -e GPT5_API_KEY=... -- uvx model-council-mcpOther MCP clients
Anything that launches a stdio server works: run uvx model-council-mcp and
pass the same environment variables.
Serving a team over HTTP
Everything above runs one copy of the server per person, launched by their own
MCP client, configured with their own keys. --http is the other shape: one
deployment holds one set of keys and answers a whole team, who configure a URL
and no secret at all.
model-council-mcp --http --host 0.0.0.0 --allow 10.20.0.0/16Colleagues then add it as a remote server, with nothing sensitive in the config:
claude mcp add --transport http model-council http://council.internal:8000/mcp// Claude Desktop and other clients
{ "mcpServers": { "model-council": { "type": "http", "url": "http://council.internal:8000/mcp" } } }deploy/ has a Dockerfile, a
compose file and a systemd unit.
Who may call
Over stdio the operating system answers this: whoever launched the process
already had the keys. HTTP removes that guarantee — one port now stands in front
of shared provider quota — so the server refuses to start on a non-loopback
address until --allow says who may reach it. Loopback needs no flag, and is
always admitted.
Flag | |
| CIDRs, bare addresses, or the names |
| reverse proxies whose |
| permit a browser |
| the one directory |
--trust-proxy is the one worth reading twice. Without it X-Forwarded-For is
ignored entirely and the peer address decides — behind nginx that is nginx, so
the allowlist matches everyone or no one. With it, the client is the rightmost
hop in the chain that is not a trusted proxy, which is what stops a caller
from writing X-Forwarded-For: 10.0.0.1 and walking straight through.
Requests carrying an Origin header are refused by default. MCP clients are not
browsers and do not send one; a web page always does. An allowlist admits every
machine on the office network, and each of those runs a browser that will issue
requests on behalf of whatever page it has open — Origin is what tells the two
apart.
Every flag has an environment variable (COUNCIL_ALLOW, COUNCIL_TRUST_PROXY,
COUNCIL_HTTP_HOST, …); --help lists them.
Material over HTTP
Over stdio, materials reads any file the process can, and needs no flag: the
caller launched this process, so it already had that access — the same reasoning
that lets a loopback bind skip --allow. Over HTTP the reasoning is gone. The
caller is whoever reached the port, a path would make one shared council into a
way to read its host's files, and what it read would leave for an external
provider. So an HTTP deployment refuses paths outright, and callers pass the
contents as text.
--materials-root /srv/council/material opens exactly one directory, resolved
before it is compared, so a symlink is judged by where it lands.
What this does not do
The allowlist is a network boundary, not an identity. Anyone inside it calls without a credential, so usage cannot be attributed to a person, rate-limited per person, or revoked for one person. That is a deliberate trade — it is what makes the client config a bare URL — but it means the network has to be a boundary you actually trust, and it does not survive contact with a VPN that admits contractors, or a CI runner on the same subnet.
If you need per-person attribution or quota, put an LLM gateway (LiteLLM, one-api, or whatever your organisation already runs) behind this server and give each caller their own virtual key there, or run this behind a reverse proxy that does SSO.
One more thing worth knowing before you announce the URL: this service forwards whatever text it is given to an external provider. A shared endpoint with no credential is a data-egress path for everyone who can reach it.
Behind a reverse proxy
ask_all with rounds=2 is a long request — several models, several attempts
each, at up to COUNCIL_TIMEOUT (180s) per call. Default proxy timeouts will
cut it off well before the server is finished:
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_buffering off; # the transport streams; buffering defeats it
proxy_read_timeout 900s;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}Then start the server with --trust-proxy set to the proxy's address, or the
allowlist will only ever see the proxy.
Configuring the council
Two layers, so several models can share one endpoint without repeating its credentials:
provider — an endpoint:
base_url+api_key+ which wire format it speaksmember — one model on some provider, addressed by a short id
Configuration comes from whichever source is most explicit: the file
COUNCIL_CONFIG points at, else the roster COUNCIL_MODELS names, else a config
file at ~/.config/model-council/config.json, else the built-in default roster.
Explicit beats discovered on purpose — a config file you left lying around must
not silently override settings a client just handed the server.
list_council() always reports which source won.
Environment variables
COUNCIL_MODELS lists the ids; each id gets variables named after it, uppercased
with non-alphanumeric characters turned into underscores (my-model →
MY_MODEL_BASE_URL).
COUNCIL_MODELS=gpt5,glm
GPT5_BASE_URL=https://your-openai-compatible-host/v1
GPT5_API_KEY=sk-xxxxxxxx
GPT5_MODEL=gpt-5
GLM_BASE_URL=https://open.bigmodel.cn/api/anthropic
GLM_API_KEY=xxxxxxxx
GLM_MODEL=glm-4.6
GLM_FORMAT=anthropicPer member: _BASE_URL, _API_KEY, _MODEL, _FORMAT, _LABEL, _WEIGHT,
_MAX_TOKENS, _TEMPERATURE, _TIMEOUT, _RETRIES, _RETRY_BACKOFF,
_HEADERS (a JSON object), _PROXY, _VISION, _CACHE, _ENABLED. Globally:
COUNCIL_TIMEOUT, COUNCIL_RETRIES, COUNCIL_RETRY_BACKOFF, COUNCIL_PROXY,
COUNCIL_CONFIG, COUNCIL_ENV_FILE, COUNCIL_MATERIALS_ROOT.
Omit COUNCIL_MODELS and the roster defaults to chatgpt,glm, reading
CHATGPT_* and GLM_*.
A config file
Better once you have more than a handful of members, or when several share an
endpoint. Set COUNCIL_CONFIG=/path/to/config.json, or drop the file at
~/.config/model-council/config.json where the server finds it on its own.
{
"providers": {
"my-relay": {
"base_url": "https://your-openai-compatible-host/v1",
"api_key": "${MY_RELAY_KEY}",
"format": "openai"
},
"zhipu": {
"base_url": "https://open.bigmodel.cn/api/anthropic",
"api_key": "${GLM_KEY}",
"format": "anthropic"
}
},
"members": [
{ "id": "gpt5", "provider": "my-relay", "model": "gpt-5", "label": "GPT-5",
"weight": 2 },
{ "id": "codex", "provider": "my-relay", "model": "gpt-5-codex", "temperature": 0.2 },
{ "id": "glm", "provider": "zhipu", "model": "glm-4.6" },
{ "id": "kimi", "base_url": "https://api.moonshot.cn/v1",
"api_key": "${KIMI_KEY}", "model": "kimi-k2" }
]
}${ENV_VAR} is expanded from the environment, so the file carries no secrets and
can be shared or committed. See examples/config.json for a
fully annotated version.
A member gets its connection one of two ways, never a mix of both: name a
provider and take that endpoint whole, or omit provider and supply
base_url + api_key + format yourself (as kimi does above). Naming a
provider and overriding one of those three is refused — that member is
disabled and list_council says why. The reason is that a partial override
would pair one endpoint's credentials with another endpoint's URL, quietly
sending your key to a host it was never issued for. Per-member headers,
timeout, temperature, max_tokens and label are not part of that
identity and stay overridable.
Fields
The first three travel together as one unit — see the rule above.
Field | Applies to | Notes |
| provider, or a member with no provider | Root the route hangs off — |
| provider, or a member with no provider | |
| provider, or a member with no provider |
|
| member | The model id sent to the endpoint |
| member | Display name in answers; defaults to the id |
| member | How far this member's opinion carries. Default 1, max 10, |
| member | Anthropic format only, where it is required. Default 8192 |
| member | Sent only when set |
| provider, member | Extra HTTP headers |
| provider, member | Seconds, per attempt. Default 180 |
| provider, member | Extra attempts a transient failure gets. Default 2, max 5, |
| provider, member | Seconds before the first retry, doubling from there. Default 1 |
| top level, provider, member | The route out. Omit to follow |
| member |
|
| provider, member |
|
| member |
|
| member | Further endpoints for the same seat, tried in order when the one before them does not answer. Max 4. See Backups |
timeout, retries, retry_backoff and proxy can also be set at the top level
of the config file, as the default every member inherits.
Weights
A council is rarely made of equals. weight says how far a member's opinion
carries — everyone is 1 until you say otherwise, and only the ratios mean
anything, so 2 and 1 is the same council as 10 and 5.
{ "id": "gpt5", "provider": "my-relay", "model": "gpt-5", "weight": 2 }It changes nothing about the call. When the weights differ, ask_all labels each
answer with its weight and ends the transcript with the ranking:
===== GPT-5 (gpt-5) · weight 2 =====
...
===== Local (qwen3-8b) · weight 0.5 =====
...
[WEIGHTS — GPT-5 2, GLM 1, Local 0.5]
These are this council's standing priors on its members, not votes. ...Weight belongs to the seat, not the endpoint, so a provider cannot set it: two
members on one relay may be a frontier model and a small fast one. 0 means
advisory — the member answers and is read, but its agreement counts for nothing.
Anything unusable (a negative, a word) falls back to 1 rather than corrupting
the ranking silently; list_council prints the effective value.
The members are never told each other's weights. A model informed that it is outranked stops arguing and starts agreeing, which costs exactly the independent dissent a council is assembled to produce — so round two carries the other answers and not their standing. The weights are for whoever reads the transcript, and they are a prior, not a vote: they break ties and decide who carries the burden of proof. A specific, checkable reason from the lowest weight still beats a bare assertion from the highest.
Backups
The same model is often reachable through more than one channel — a company gateway, a public relay, a spare key. Listing each as its own member is the wrong shape: it puts one model in the room several times, so it answers several times, is weighed several times, and in round two argues with itself.
backups are further endpoints for the same seat, tried in order when the one
before them does not answer:
{
"id": "sol", "provider": "internal", "model": "gpt-5-codex", "label": "Codex",
"backups": [
{ "provider": "my-relay" },
{ "base_url": "https://another-relay.example/v1", "api_key": "${SPARE_KEY}",
"model": "gpt-5-codex-latest" }
]
}One id, one label, one weight, one vote. You never address a backup —
ask(model="sol") reaches whichever of the three is up.
A backup inherits what the seat settled about the model — model, max_tokens,
temperature, vision, timeout, retries — so the ordinary case, the same
model on a second relay, is the one line above. Name model only when the other
channel calls it something else. Three things are never inherited:
the connection (
base_url,api_key,format) — that is the point of a backup. It either names aproviderand takes all three from it, or brings all three itself. Half of each is refused, for the reason in A config file.headers— a header written for one endpoint is routinely a credential for it.proxy— the route belongs to the host, not to the seat. A seat pinnedproxy: falsefor an internal gateway would otherwise send its public backups around the very proxy they need. A backup takes its route from its own provider, or from the council's default.
When a seat falls through. Not only on a dead connection: a revoked key, a
model id the relay stopped carrying, a gateway answering 200 with something
that is not an answer. From the seat's point of view these are one event — this
way in is not currently a way to that model, and the config named another.
Transient failures (Retries) are still retried on the connection they
happened on first, so a backup is for an endpoint that is out, not one that is
slow to say yes. Put retries: 0 on the primary if you would rather fail over
than wait.
Which endpoint answered is printed with the answer. A backup is frequently a different model id and sometimes a different model, and a council read as a comparison of models must not quietly compare something else:
===== Codex (gpt-5-codex-latest — backup 2, after the primary did not answer) =====list_council shows the chain indented under each seat:
id label model format route endpoint tries status
sol Codex gpt-5-codex openai direct https://gateway.internal/v1 3 × 180s ready
↳ backup 1 gpt-5-codex openai env https://your-relay/v1 3 × 180s standing by
↳ backup 2 gpt-5-c... openai env https://another-relay/v1 3 × 180s standing byprobe_models probes every link, so a backup that has quietly stopped carrying
its model is visible before the day it is needed. A seat where nothing answered
reports each connection it tried and why.
Backups are a config-file feature; the environment-variable roster has no
equivalent. Max 4 per seat — a seat is one model's opinion, not a
high-availability cluster, and ask_all waits on the slowest chain to finish
failing.
Proxies
Every member follows this machine's HTTP_PROXY/HTTPS_PROXY, which is the
right default until a council spans hosts that do not share a route — a public
API that only answers through the proxy, an internal gateway the proxy cannot
see. One setting cannot be right for both, so the route is chosen per seat.
proxy takes four values, on a member, on a provider, or at the top level of the
config file as the council's own default:
Value | The route |
omitted | Follow the council's |
| Straight out, ignoring both |
a URL | Through that proxy — |
| Back onto |
Most specific wins: member, then provider, then the council, then the environment. So a council that is mostly behind a proxy, with two seats that must not be, says it once and then twice:
{
"proxy": "http://127.0.0.1:7890",
"providers": {
"internal": { "base_url": "https://gateway.internal.example/v1",
"api_key": "${INTERNAL_KEY}", "proxy": false }
},
"members": [
{ "id": "gpt5", "provider": "my-relay", "model": "gpt-5" },
{ "id": "inhouse", "provider": "internal", "model": "some-internal-model" },
{ "id": "local", "base_url": "http://127.0.0.1:11434/v1", "api_key": "-",
"model": "qwen3-8b", "proxy": false }
]
}The same three moves through environment variables — COUNCIL_PROXY for the
council, <ID>_PROXY for one member:
COUNCIL_PROXY=http://127.0.0.1:7890
INHOUSE_PROXY=direct
LOCAL_PROXY=directlist_council prints the route it settled on, as env, direct, or the
proxy URL, in a column that appears only when the members can differ:
network: HTTPS_PROXY=http://127.0.0.1:7890 in this server's environment — the members whose route is 'env' go through it
id label model weight format sees route endpoint tries status
gpt5 GPT-5 gpt-5 1 openai text+img env https://your-host/v1 3 × 180s ready
inhouse Inhouse internal 1 openai text+img direct https://gateway.internal/v1 3 × 180s ready
kimi Kimi kimi-k2 1 openai text+img http://127.0.0.1:7890 https://api.moonshot.cn/v1 3 × 180s readyA password inside a proxy URL is masked wherever it is printed — the table, the warnings, the text of a connection error.
A proxy that cannot be used is caught when the roster is read, not on the
first call. 127.0.0.1:7890 with the scheme left off is read as
http://127.0.0.1:7890 and says so in the warnings; a scheme nothing can dial
parks that member with the reason next to it in list_council, rather than
letting it fail every call with a ValueError raised from inside a request. The
member is parked rather than quietly sent another way: a proxy is named because
someone wants the traffic to go that way.
socks5:// needs one package httpx does not install by default. Install the
extra — pip install 'model-council-mcp[socks]', or
uvx --from 'model-council-mcp[socks]' model-council-mcp — or that member is
parked with a note saying exactly this.
Wire format notes
formatis not inferred from the URL. Pointingbase_urlat an Anthropic-style endpoint without also settingformat: "anthropic"leaves the member on the OpenAI format, and every call fails. This is the single most common misconfiguration.Anthropic endpoints: the server posts to
{base_url}/v1/messages, sobase_urlshould not already include the/v1.OpenAI-compatible endpoints: the server uses
/chat/completions, never/responses. Some gateways expose both, but/responsesmay inject a provider-chosen system persona, which is wrong for a general-purpose advisor.A system proxy is followed by default. If a member sits on a network your proxy cannot reach — an internal gateway, typically — it fails with a bare
ConnectErrorthat never mentions a proxy. Give that member or provider"proxy": falseand it connects directly, while everyone else keeps using the proxy; see Proxies. Connection errors name the route the member was on, so the three policies do not all fail the same way.Model ids move fast. Run
probe_modelsto see what an endpoint actually offers today.
Using it
Things worth typing to the chair:
"Answer this yourself, then
ask_alland give me a table of where you all agree and disagree.""Ask gpt5 and glm this, then critique both answers and tell me which is more correct and why."
"Run two rounds of
ask_allon this, then tell me who changed their mind and what actually settled it.""Ask only glm — I want a second opinion on this one file."
Local development
uv syncCopy .env.example to .env, fill in real values, then:
uv run python tests/test_smoke.pyThe smoke test checks both configuration paths offline; with a usable .env it
finishes with a live round-trip. To point a client at your working copy, use the
model-council-mcp script inside your environment instead of uvx.
Troubleshooting
Server doesn't appear — check the client's MCP logs (Claude Desktop:
~/Library/Logs/Claude/mcp*.log). The server writes configuration warnings to stderr at startup.A tool answers
[... is not configured]— that member is missingbase_url,api_key, ormodel. Runlist_councilfor a per-member breakdown.HTTP 401 — wrong key, or a key the provider has disabled.
HTTP 404 — wrong
base_url, or the wrongformatfor that endpoint.The model id is rejected — run
probe_models.A member says
gave up after N attempts— it failed transiently every time. The error text is the last one the endpoint gave.list_councilshows each member's budget asattempts × timeout.A call takes far longer than the timeout — retries multiply it: three attempts at 180s each is a worst case of ~9 minutes plus backoff. Lower
timeout, orretries, for a member you would rather have fail fast.A long
ask_allfails with a timeout while the server is still working — that line is the client's, not this server's, andCOUNCIL_TIMEOUTdoes not move it: it only decides how soon a member gives up. The reference MCP SDK draws it at 60 seconds of silence, which one slow member clears unaided. This server reports progress every ten seconds so that a client which honours it restarts its clock; a client that does not needs its own setting (Claude Code takes a per-servertimeoutin milliseconds in its MCP config, orMCP_TOOL_TIMEOUTfor all of them). Note whose limit applies: it belongs to the client that spawned the server process, so a server registered with a desktop app and bridged into another tool is held to the desktop app's number, not to the one you set where you are typing.probe_modelsreports that a seat would not list — read the note it prints under the table. An endpoint that refusesGET /modelsis often a relay that serves only the chat route, and that is not a member that cannot answer.askis what settles it.One member fails with a bare
ConnectErrorwhile the rest are fine — it is usually the route, not the endpoint. The error names which route that member was on;list_council'sroutecolumn shows all of them at once, and Proxies is how to change one.
License
MIT
Available Tools
6 toolsaskA
Ask ONE member of the council a question and return its answer.
model is the member's short id; the schema lists the ones this council has, and
list_council describes them.
The member is stateless and cannot see this conversation, so prompt must carry
everything it needs — including any other model's answer you want it to critique.
Optionally set system to steer its role or output format.
materials hands the council the thing the question is about — a spec, a log, a diff, a screenshot — instead of you pasting it into prompt. Give {path, label} for a file, or {text, label} for something with no file behind it. Prefer a path: the members are then handed the file's exact bytes rather than your reproduction of them, you do not spend a copy of the whole document writing this call, and every member and every round get an identical copy, which is both what makes their answers comparable and what an endpoint's cache can match. Images go this way too, and are the case that matters most: describe a screenshot in prose and every member inherits the same description, so anything you misread is misread by the whole council at once.
| Name | Required | Description | Default |
|---|---|---|---|
| model | Yes | ||
| prompt | Yes | ||
| system | No | ||
| materials | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden. It discloses statelessness, lack of conversation context, how materials are delivered to every member/round, image handling, and caching implications — all beyond the minimal description.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Every sentence earns its place: the core action is front-loaded, then parameters are explained in natural order. The materials rationale is detailed but directly serves correct usage, not padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return-value details need not be repeated. The description covers the tool's action, model selection, prompt construction, and material handling — sufficient for an agent to invoke it correctly without additional documentation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, and the description fully compensates. It explains model as a short ID, prompt as needing to carry full context, system as optional steering, and materials' path/text/label semantics including when to prefer paths. This adds essential meaning the schema omits.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb and resource: 'Ask ONE member of the council a question and return its answer.' The 'ONE member' phrasing immediately distinguishes it from the sibling ask_all, and the purpose is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear contextual guidance: the member is stateless so prompt must be self-contained, and materials should be passed by path when possible. It does not explicitly name alternatives like ask_all or revise, but the 'ONE member' framing makes the boundary clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
ask_allA
Ask several members the SAME prompt in parallel, returning their answers side by side and labeled by model.
By default every configured member answers. Pass models to ask only some of
them; the schema lists the valid ids.
rounds (1-3, default 1) runs a real discussion. With rounds=2 the members
first answer independently, then each is asked again — this time carrying the
question plus every answer from round 1, its own and the others', verbatim — and
asked to revise. Members are stateless, so carrying the previous round back to
them is the whole mechanism: without it a second round is just the same question
asked twice. The transcript returns round by round, so you can see who moved and
who held their ground.
One round is a survey of opinion. Two is worth the extra latency and tokens when the answers are likely to disagree and the disagreement is the interesting part.
rounds is chosen before anything has been asked, which is the one thing wrong
with it: you commit to a second round without having read the first, and a
council that turns out to agree costs exactly what one still arguing would. When
you would rather look first, ask with rounds=1 and then call revise with the
answers — it runs one more round on demand, as many times as you judge it worth,
and it has no ceiling. Reading before you buy is usually the cheaper of the two:
one more round is another full call per member, while revise costs you only the
answers written back into it.
guests seats answers you already have. If you spawned a subagent on this same
question, or formed your own view first, pass it here as {label, text} and it
joins the table: it appears in round 1 beside the members, and from round 2 the
members are shown it verbatim and argue with it. This is the difference between
an answer that is in the discussion and one that is merely next to it — without
it, the models never learn your subagent had an opinion. Pass the text verbatim,
not a summary; a summary is not what you want critiqued.
A guest speaks once and does not revise, so it appears in round 1 only. The transcript says so, and says it is not a retraction. From round 2 the members are told to answer its strongest point rather than pass over it: a seat that cannot restate itself is the one a discussion drops by default, and dropping it reads in the transcript exactly like answering it.
steelman seats a standing objection. A council mostly agrees, and its agreement
is the least informative thing it produces — the strongest case against a plan is
not volunteered by members who think the plan is fine. Pass {} and one member
writes that case each round, against whatever the table has converged on, and it
goes back to everyone as an ordinary anonymous answer:
ask_all(prompt, rounds=3, steelman={})Note what is and is not assigned. No member is told to argue a side it does not
hold, so what the members say is still what they think; the assignment lives in
one extra call they are not told about, and they answer it as they answer any
other seat. It speaks every round rather than once, because an objection that
cannot reply to its own rebuttal is quoted rather than represented, and by the
third round the table is arguing with its paraphrase of it. tenure buys fewer
rounds than that, and when the seat is retired early the transcript says it was
retired by configuration rather than answered — an unexplained silence reads as a
position abandoned. You are told what the seat was and who wrote it; the members
are not, because a model that knows an argument was commissioned discounts it
instead of answering it. Read what it says as the strongest objection this
council can produce to order, never as evidence that anyone holds it.
Members may carry different weights — how much this council trusts each one. When they do, every answer is labeled with its weight and the transcript ends with the ranking and how to read it. The members are never told each other's weights; a model told it is outranked stops arguing, and its dissent is what you came for.
materials hands the council the thing the question is about — a spec, a log, a diff, a screenshot — instead of you pasting it into prompt. Give {path, label} for a file, or {text, label} for something with no file behind it. Prefer a path: the members are then handed the file's exact bytes rather than your reproduction of them, you do not spend a copy of the whole document writing this call, and every member and every round get an identical copy, which is both what makes their answers comparable and what an endpoint's cache can match. Images go this way too, and are the case that matters most: describe a screenshot in prose and every member inherits the same description, so anything you misread is misread by the whole council at once. It is carried into every round for you, so a discussion
about one document costs you the path once.
| Name | Required | Description | Default |
|---|---|---|---|
| guests | No | ||
| models | No | ||
| prompt | Yes | ||
| rounds | No | ||
| system | No | ||
| steelman | No | ||
| materials | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries full disclosure burden and meets it. It reveals hidden mechanics: members are stateless, round 2 only works because previous answers are carried back verbatim, steelman assignments are hidden from members, and weights are never shared. It also states what the transcript will and won't say (e.g., a retired steelman is labeled as retired by configuration, not as a retraction).
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but deliberately organized: it front-loads the core behavior, then devotes one section to each advanced feature in a consistent order. Some conceptual rationale could be trimmed, but for a seven-parameter tool with this many interaction effects, the length earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity and the presence of an output schema, the description covers the full decision space: defaults, rounds mechanics, guest behavior, steelman tenure and concealment, weights, and material handling. Nothing an agent needs to decide whether to call it or how to shape a call is left unexplained.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The description adds substantial meaning beyond the 0%-coverage schema for `models`, `rounds`, `guests`, `steelman`, and `materials`, including defaults and interaction effects. However, it never mentions the `system` parameter, and the guest `weight` field is absent from the prose even though it exists in the schema, leaving small gaps that the schema cannot fill at 0% coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence names a specific verb and resource — 'Ask several members the SAME prompt in parallel' — and states the output shape ('answers side by side and labeled by model'). This clearly separates it from siblings like `ask` and `revise` without needing to open their schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit decision rules: one round is a survey, two rounds are for disagreement, and 'when you would rather look first, ask with rounds=1 and then call revise'. It also tells when to use `guests` (answers already obtained elsewhere) and `steelman` (a council that mostly agrees), so an agent can route itself correctly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_councilA
List the council's members: their ids, labels, target models, weights, wire format, endpoint, call budget, and whether each one is ready to answer.
weight is how much this council trusts each member, relative to the others;
everyone is 1 unless the roster says otherwise, and ask_all reports it
alongside the answers whenever they differ.
tries is how many attempts a call gets and how long each may take, so a
member that is slow or that keeps being retried is visible here.
sees is whether this member may be shown an image in materials. The line
above the table says whether this server will read materials paths at all,
which depends on how its operator runs it.
route is how this member reaches its endpoint — env follows the proxy in
the server's environment, direct ignores it, and a URL is a proxy set for
that member alone. It appears only when the members can differ; passwords in
a proxy URL are masked.
A member may have backups: further endpoints for the same seat, indented
under it as ↳ backup 1, ↳ backup 2 and tried in that order when the one
above does not answer. They are the same member — same id, same label, one
vote — so you never address a backup directly; the seat's id reaches
whichever of them is up. Read their model column: a backup is often the
same model under a different id, and sometimes not the same model at all,
and ask_all names the one that actually answered above each answer.
Cheap and local — makes no network calls. Use this to find out which ids you
may pass to ask and ask_all, or to explain a configuration problem.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full behavioral burden and does so thoroughly. It discloses that the tool makes no network calls, explains the meaning of each output field, and covers edge cases like backups, proxy routing, password masking, and conditional columns.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but each paragraph explains a distinct cluster of output fields and edge cases. It front-loads the core action and field list, then uses compact and consistent formatting for backups, routing, and usage guidance.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with no parameters and an output schema, the description is complete: it names every visible field, defines non-obvious values, explains backup behavior and proxy routing, and gives concrete use cases. There are no meaningful gaps an agent would need filled to call it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema has zero parameters and 100% schema description coverage trivially, so there is no parameter semantics for the description to add. The baseline of 4 for zero-parameter tools applies, and the description does not need to compensate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and resource: "List the council's members" and enumerates the exact fields returned. It also positions itself against siblings by saying it reveals which ids may be passed to ask and ask_all, so an agent can distinguish it from the other tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives clear usage context: "Cheap and local — makes no network calls" and "Use this to find out which ids you may pass to ask and ask_all, or to explain a configuration problem." It names the relevant alternatives, though it does not explicitly spell out when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
probe_modelsA
Ask a member's endpoint which model ids it actually exposes, by calling its /models route.
Pass model to probe one member, or omit it to probe every configured member.
Use this when a call fails with an unknown-model error, or to discover what else a provider offers — model ids move fast.
Not every endpoint has that route. A seat that refuses the listing is not thereby a seat that cannot answer — this establishes only the first, and the output says so rather than leaving you to read the second into it.
| Name | Required | Description | Default |
|---|---|---|---|
| model | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full behavioral burden. It discloses the HTTP-route behavior, the difference between omitting and passing model, and the important nuance that an endpoint refusing the listing does not mean it cannot answer. This is substantive behavioral context.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact, front-loaded with the core action, and each sentence earns its place: usage, parameter behavior, motivation, and an important caveat. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given one optional parameter, an output schema, and no annotations, the description covers all essential context: what the tool does, which parameter selects what, when to rely on it, and how to interpret a refusal. Nothing critical is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must supply meaning for model. It does so clearly: pass model to probe one member, omit it to probe every configured member. This adds real semantic value beyond the bare enum/default in the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb + resource: ask a member's endpoint which model ids it exposes via the /models route. It clearly separates probe_models from siblings like ask or ask_all by focusing on endpoint/model-id discovery rather than answer generation.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly states when to use the tool: after an unknown-model error or to discover provider offerings. It also explains the parameter-driven choice between probing one member or all members, though it does not explicitly name alternatives or exclusion cases.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
reviseA
Run ONE more round of an existing discussion: show every member what was said last round and ask it to revise.
This is ask_all's rounds turned inside out, so that you can drive them. Two
reasons to want that.
You would rather decide after reading. ask_all(rounds=2) commits to the
second round before the first one exists. Ask with rounds=1, read what comes
back, and call this only if the disagreement is worth another call per member —
or call it three more times if they are still moving. There is no round ceiling
here, unlike ask_all, because every round is one you chose to pay for.
A voice in the discussion is one only you can produce — a subagent you spawned, or your own answer — and you want it to be a full member rather than a one-off: something that answers, reads the others, and revises alongside them.
The loop, in the second case:
ask_all(prompt)— the members answer round 1 ...and you produce your subagent's round-1 answer yourselfrevise(prompt, answers=[...])— pass EVERY round-1 answer, the members' and your subagent's; the members come back revised ...and you re-run your subagent on the same materialrepeat as long as it is still moving
Each entry in answers is {text, model} for a member's own answer, or
{text, label} for an outside one. Naming the member matters: that is what lets
it see its previous answer as its own and revise, instead of answering fresh.
round is which round the answers you are passing came from, so the members are
told where they are. Pass answers verbatim, never summarised.
Pass the same materials you passed to ask_all, every round. The members are
stateless: a document they were shown last round does not exist for them in this
one, and a council revising from memory it does not have will revise from the
other answers alone.
| Name | Required | Description | Default |
|---|---|---|---|
| round | No | ||
| models | No | ||
| prompt | Yes | ||
| system | No | ||
| answers | Yes | ||
| materials | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden and delivers: it discloses that members are stateless, that answers must be passed verbatim, that model-named answers are shown back as the member's own so they revise rather than start over, and that each round is paid for. It also walks through the expected loop so an agent knows exactly what will happen.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is longer than average but earns its length with dense, useful information: a summary sentence up front, two motivating use cases, a numbered loop, and parameter-specific warnings. Every paragraph adds value, and the structure makes navigation easy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity, lack of annotations, and zero top-level schema coverage, the description covers almost everything needed to call it correctly: when to use it, how to construct `answers`, what to pass every round, and the statelessness caveat. The only gap is the undocumented `models` and `system` parameters, which keeps it just short of fully complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The top-level schema has 0% description coverage, so the description compensates well for the key parameters: `answers`, `round`, `materials`, and `prompt` all receive operational meaning. However, `models` and `system` remain undocumented, so the compensation is not complete.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence states the exact action: 'Run ONE more round of an existing discussion: show every member what was said last round and ask it to revise.' This clearly distinguishes it from ask_all (which commits to rounds in advance) and ask (one-off), and the rest of the description reinforces that distinction.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly contrasts with ask_all: 'This is ask_all's rounds turned inside out' and explains two concrete use cases: deciding after reading each round, and injecting a subagent or one's own voice as a full member. It also gives the loop steps and notes there is no round ceiling unlike ask_all. This is exemplary when-to-use guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
revision_promptA
Build the exact prompt a seat should be given for the next round — for the one seat this server cannot ask itself: yours.
revise asks the members. Your subagent is yours to re-run, and how you prompt it
decides whether it revises at all: handed the original question again, it will
reproduce its previous answer, and nothing in the transcript will show that the
seat stopped participating. Handed this, it revises on exactly the terms the
members did — same framing, same instruction to hold a position it still believes
against the majority, which is the sentence that keeps a council from collapsing
into agreement.
Pass the same prompt, answers, round and materials you are passing to
revise, plus seat: the label (or member id) of the seat to write for. Cheap
and local — makes no network calls, so run it alongside revise rather than
after it.
Material is named in the prompt rather than pasted into it, at the position the members were given it. Open those files for your seat before you hand it the rest: a seat that revises without the document is arguing about something it has not read, and the transcript will not show it.
| Name | Required | Description | Default |
|---|---|---|---|
| seat | Yes | ||
| round | No | ||
| prompt | Yes | ||
| answers | Yes | ||
| materials | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations provided, the description carries the full burden. It discloses that the tool 'makes no network calls', that re-running a subagent on the original question would reproduce the previous answer, and that material is named rather than pasted. It also warns about the consequence of opening files when preparing the prompt, which is important behavioral context beyond the schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with the core purpose and then adds rationale, parameter mapping, and material-handling guidance in a logical order. It is somewhat verbose and repeats the 'transcript will not show' idea twice, but most sentences earn their place given the tool's subtle behavior.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with no annotations and no top-level schema descriptions, this description is unusually complete. It explains what the tool does, why it is needed, how to call it, what arguments to pass, what side effects are absent, and how materials should be handled. An output schema exists, so the lack of a described return format is not a gap.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage for top-level parameters is 0%, and the description compensates by mapping parameters to the sibling call: 'Pass the same prompt, answers, round and materials you are passing to revise, plus seat.' It also clarifies that 'seat' is 'the label (or member id) of the seat to write for.' It does not independently explain prompt, answers, or round, but the cross-reference to revise makes the semantics usable.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific verb and object: 'Build the exact prompt a seat should be given for the next round.' It immediately contrasts itself with 'revise' ('revise asks the members'), so an agent can distinguish this prompt-builder from the council-asking sibling. It also names the exact target seat ('the one seat this server cannot ask itself: yours').
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says which sibling to compare against and exactly what to pass: 'Pass the same prompt, answers, round and materials you are passing to revise, plus seat.' It even gives timing guidance: 'run it alongside revise rather than after it.' This is explicit, actionable usage guidance.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
TDQS
The tools are mostly distinct: ask targets one member, ask_all targets many, revise drives an existing discussion forward, and list_council/probe_models serve different inspection purposes. Some adjacency exists between ask_all and revise, and between list_council and probe_models, but the descriptions explicitly separate these cases.
Naming is mixed: bare verbs like ask and revise, a phrasal verb like ask_all, verb_noun pairs like list_council and probe_models, and a noun-phrase tool name in revision_prompt. The names are still readable and domain-grouped, but they do not follow one consistent pattern.
Six tools is well-scoped for a model-council server: one-member queries, multi-member discussion, on-demand revision, prompt construction, roster inspection, and endpoint probing all earn their place. There is no filler and no glaring redundancy.
The core workflow is fully covered: ask a single member, ask the whole council, revise iteratively, build prompts for an external seat, list members, and probe available model ids. Configuration of members is intentionally operator-side, so no create/update/delete tools are expected.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
MCP server for building and testing AI agents with multi-model experimentation and insights.
An MCP memory server. One memory your agents share — across models, devices and apps.
MCP server for AI dialogue using various LLM models via AceDataCloud
Real-time chat for AI agents. Claude Code, Cursor, Cline and Codex join channels over MCP.
Related MCP Servers
- AlicenseAqualityCmaintenanceAn MCP server that enables users to query, compare, and synthesize responses from multiple local and cloud LLMs simultaneously using existing subscriptions. It provides tools for parallel model evaluation, consensus polling with an LLM-as-judge, and response synthesis across different model providers.81515MIT
- AlicenseNot gradedqualityCmaintenanceAn MCP server that enables an AI orchestrator to convene a council of other AIs via their CLIs, deliberate, and synthesize consensus.4GPL 3.0
- AlicenseNot gradedqualityDmaintenanceMCP server for AI agents to conduct multi-LLM roundtable discussions, returning structured common, divergent, and unique perspectives.MIT
- AlicenseAqualityBmaintenanceMCP server that reduces confirmation bias in LLMs by orchestrating structured debates between asymmetric context sessions.13MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Totti0135/model-council'
If you have feedback or need assistance with the MCP directory API, please join our Discord server