ai-discuss
by ponthepmk
README.md
# ai-discuss MCP server
An [MCP](https://modelcontextprotocol.io) server that lets a host AI agent
(**Claude Code, opencode, or Codex**) trigger a **multi-agent debate**. The host
calls the `discuss` tool with a topic + code context; the server fans the
question out to several configured AI models, runs an N-round debate where the
agents critique and refine each other's answers, then a synthesizer produces a
consensus recommendation with **ranked, scored options** — and returns it so the
host can keep coding.
Models are reached through OpenAI-compatible **providers** — use **OpenRouter**
(cloud: Claude, GPT, Gemini, DeepSeek, …), **Ollama** (local, keyless), or both
at once in the same debate.
## How it works
```
Claude Code / opencode / Codex ──MCP stdio──► ai-discuss
│ round loop (fan-out, timeouts, error isolation)
┌──────────────────────┼───────────────────┐
▼ ▼ ▼
OpenAICompatAdapter OpenAICompatAdapter Synthesizer
(OpenRouter / cloud) (Ollama / local) (a chosen participant)
│ │ │
└───────────────────────┴───► full markdown transcript on disk
```
- **Round 1:** each participant answers independently.
- **Rounds 2..N:** each participant sees the others' previous answers
(anonymized by default) and critiques / refines.
- **Synthesis:** the synthesizer scores each option 0–100 and ranks them, with
reasoning, consensus, and unresolved disagreements.
Output is returned three ways: a concise summary for the host agent, a
`structuredContent` object, and a complete markdown transcript written to disk.
## Install & build
```bash
npm install
npm run build
```
## Configure participants
Copy the example config and edit it:
```bash
cp ai-discuss.config.example.json ai-discuss.config.json
```
The config has a `providers` map (OpenAI-compatible endpoints) and a list of
participants that each pick a `provider` + `model`:
```jsonc
{
"providers": {
"openrouter": { "baseURL": "https://openrouter.ai/api/v1", "apiKeyEnv": "OPENROUTER_API_KEY" },
"ollama": { "baseURL": "http://localhost:11434/v1", "apiKeyEnv": null }
},
"participants": [
{ "id": "claude", "provider": "openrouter", "model": "anthropic/claude-sonnet-4" },
{ "id": "qwen-local", "provider": "ollama", "model": "qwen3.6" },
{ "id": "mock", "type": "mock", "enabled": false }
]
}
```
| participant | how it connects | key fields |
| ----------- | ----------------------------------------------------- | ---------- |
| model | an OpenAI-compatible provider (default `type`) | `provider`, `model`, `temperature?`, `maxTokens?` |
| `mock` | deterministic echo — for credit-free testing | `reply?` |
- API keys are **never** stored in config — a provider's `apiKeyEnv` names the
env var that holds the key. `apiKeyEnv: null` marks a **keyless** provider
(e.g. local Ollama).
- An enabled participant whose provider needs a key that isn't set is skipped at
runtime — it never crashes the run.
- Add or swap a discussant by editing its `model` (see model ids via the
`list_models` tool, [openrouter.ai/models](https://openrouter.ai/models), or
`ollama list`).
Top-level options: `defaultRounds`, `defaultSynthesizer`, `transcriptDir`,
`perParticipantTimeoutMs`, `maxConcurrency`, `anonymizePeers`, `apiRetries`.
## Register with a host
The server is a standard stdio MCP server, so it works with any MCP host. Build
first (`npm run build`), then register. Set `OPENROUTER_API_KEY` if you use
OpenRouter; for Ollama just have `ollama serve` running (no key).
**Claude Code** — `.mcp.json` in the project, or:
```bash
claude mcp add ai-discuss --env OPENROUTER_API_KEY=sk-or-... \
-- node /absolute/path/to/Ai-discuss-mcp/dist/index.js
```
**opencode** — `opencode.json`:
```jsonc
{
"mcp": {
"ai-discuss": {
"type": "local",
"command": ["node", "/absolute/path/to/Ai-discuss-mcp/dist/index.js"],
"enabled": true,
"environment": { "OPENROUTER_API_KEY": "sk-or-..." }
}
}
}
```
**Codex** — `~/.codex/config.toml`:
```toml
[mcp_servers.ai-discuss]
command = "node"
args = ["/absolute/path/to/Ai-discuss-mcp/dist/index.js"]
env = { OPENROUTER_API_KEY = "sk-or-..." }
```
## Tools
### `discuss`
| field | type | notes |
| ---------------- | ---------- | ----- |
| `topic` | string | required — the question/decision to debate |
| `context` | string? | code, constraints, background |
| `options` | string[]? | candidate approaches to rank (else participants propose their own) |
| `rounds` | number? | 1–6, defaults to config |
| `participants` | string[]? | filter to these ids, defaults to all enabled |
| `synthesizer` | string? | participant id for synthesis, defaults to config |
| `writeTranscript`| boolean? | default `true` |
Returns `recommendation`, `rankedOptions[{option, score, reasoning, risks}]`,
`consensus`, `disagreements`, `participantsUsed`, `participantsFailed`, `rounds`,
`synthesizerId`, `degraded`, and `transcriptPath`.
### `list_participants`
Lists configured participants (id, provider, model, enabled/available, default
synthesizer). Cheap — reads config only, no model calls. Useful before calling
`discuss`.
### `list_models`
Queries each configured provider for the model ids it can serve (OpenRouter
`/models`, Ollama `/api/tags`). Useful to discover valid model names. Optional
`provider` arg narrows to one provider.
## Example
> Claude Code, after scaffolding a trading bot, calls:
```json
{
"name": "discuss",
"arguments": {
"topic": "Choose an order-execution strategy for a momentum intraday stock bot to minimize slippage on mid-cap tickers.",
"context": "Python bot, Alpaca API, ~50 trades/day, $5k-$20k positions, currently naive market orders.",
"options": ["Market orders", "Marketable limit orders (5bps cap)", "TWAP over 60s", "Adaptive VWAP slices"],
"rounds": 3,
"synthesizer": "claude"
}
}
```
The server returns a ranked recommendation and a transcript path, and the host
continues editing the execution module.
## Development
```bash
npm run dev # tsx watch (no rebuild loop)
npm test # vitest unit suite (no network / no credits)
npm run inspect # MCP Inspector against the built server
npm run typecheck # tsc --noEmit
```
### Credit-free end-to-end
Set every participant (including the synthesizer) to `type: "mock"` and run the
server through `npm run inspect` or any MCP client. The full pipeline runs,
writes a transcript, and returns valid `structuredContent` without any API
calls. (With mock participants the synthesizer can't emit JSON, so you'll see
`degraded: true` — that exercises the fallback path.)
## Design notes
- **Adapter pattern** — the orchestrator only ever calls `participant.ask()`; it
never knows whether a participant is a real model or a mock. One
`OpenAICompatAdapter` serves every provider (OpenRouter, Ollama, …), differing
only by `baseURL`, optional key, and headers.
- **Error isolation** — `ask()` never throws; failures are encoded in the
result. Each round fans out with `Promise.allSettled` + per-participant
timeout/abort, so one dead participant degrades but never aborts the run. A
participant that fails one round is still invited to the next.
- **Always-valid output** — the synthesizer is asked for strict JSON, retried
once, and finally falls back to a mechanical synthesis so the tool always
returns schema-valid structured content.
- **stdout is sacred** — all logging goes to stderr only; stdout carries the MCP
JSON-RPC stream.
TDQS
A4.2/5.0
Scored across 3 tools
Disambiguation5/5
Each tool serves a distinct purpose: discuss runs the debate, list_models discovers available models, and list_participants shows configured participants. No functional overlap.
Naming Consistency5/5
All tools use consistent snake_case verb_noun naming (discuss, list_models, list_participants), with a clear pattern.
Tool Count5/5
Three tools is well-scoped for this focused server; each tool plays an essential role without redundancy.
Completeness3/5
Tools cover the main discussion task and listing of resources, but lack create/update/delete operations for participants or models, leaving configuration management to external means.
Maintenance
ActivityInactive
ResponsivenessNo issues