Peer Review MCP Server
Integrates with Google's Gemini models (via OpenAI-compatible API) to serve as peer sources in a weighted quorum system for peer review.
Supports local or self-hosted models via Ollama as peer sources for the weighted quorum mechanism.
Enables querying OpenAI models (e.g., GPT) as peer sources, with configurable weights, cost tiers, and consensus evaluation.
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., "@Peer Review MCP ServerWhat is the consensus on the best programming language for AI?"
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.
Peer Review MCP Server
A provider-agnostic MCP (Model Context Protocol) server that asks a weighted quorum of external LLM peers the same question and returns a consensus answer with a machine-consumable certainty score. It supports any OpenAI-compatible or Anthropic-compatible API, stacks sources in cost tiers, and degrades visibly (never silently) when a source is unavailable.
Features
Weighted quorum with a certainty score — peers vote with configurable weights; every
peer_reviewresponse carries a machine-consumablecertaintyScore.Provider-agnostic — two wire formats (OpenAI
chat/completions, Anthropicv1/messages) cover OpenAI, Gemini, OpenRouter, Ollama, vLLM, Anthropic, and most gateways.Cost tiers with escalation — tier 1 sources are consulted first; higher tiers only fan out on quorum shortfall.
Visible degradation — failed or unavailable sources are itemized in the result, never silently dropped; an unmet quorum is a reported outcome, not an error.
No vendor SDKs — all providers are reached over plain HTTPS
fetch.
Related MCP server: ypollak2/llm-router
Table of Contents
Requirements
Node >= 20
Installation
git clone https://github.com/raydollete/peer-review.git && cd peer-review
npm install
npm run build # emits dist/app.jsQuick Start
Create a config file (default path ./peer-review.config.json):
{
"thresholds": { "tier1": 2, "tier2": 4 }, // weight targets — see "How tiers, weights, and thresholds interact"
"arbiter": "gpt-large", // source name used for consensus evaluation
"sources": [
{ "name": "gpt-large", "apiType": "openai", "baseUrl": "https://api.openai.com/v1", "model": "gpt-5.2", "apiKeyEnv": "OPENAI_API_KEY", "weight": 2, "tier": 1 },
{ "name": "claude", "apiType": "anthropic", "baseUrl": "https://api.anthropic.com", "model": "claude-fable-5", "apiKeyEnv": "ANTHROPIC_API_KEY", "weight": 2, "tier": 2 }
]
}Then register the server in Claude Code:
claude mcp add peer-review \
--env PEER_REVIEW_CONFIG=/absolute/path/to/peer-review.config.json \
--env OPENAI_API_KEY=sk-... \
-- node /absolute/path/to/peer-review/dist/app.jsConfiguration
The server reads a JSON config file from PEER_REVIEW_CONFIG (default ./peer-review.config.json). See peer-review.config.example.json.
Config file fields
Field | Type | Required | Description |
| object | yes | Map of tier → positive integer weight target. Keys may be |
| string | yes | Name of the source used to evaluate agreement between peer responses. Must match a |
| string | yes | Unique source name (used in |
|
| yes | Wire format: |
| URL | yes | API base URL. For |
| string | yes | Model id sent to the provider. Never client-suppliable. |
| string | exactly one of these two | Name of the environment variable holding a static API key. If the variable is unset the source is marked unavailable (the server still starts). |
| string | exactly one of these two | Command executed (via |
| positive int | yes | Weight this source contributes to quorum when it agrees with the consensus. |
| positive int | yes | Cost tier. Tier 1 is consulted first; higher tiers only on shortfall. |
Startup fails fast on: an unreadable/invalid config file, an unknown arbiter name, a source tier without a threshold, duplicate source names, a source named caller (reserved for the callerAnswer rating channel), or a source declaring both or neither of apiKeyEnv/apiKeyCommand. Credential material never appears in config, logs, or responses.
Environment variables
Variable | Default | Description |
|
| Path to the config file. |
|
| Per-source request timeout (includes retries). |
|
| Hard overall deadline for one |
|
| Max output tokens injected server-side into every peer call. |
|
| Cache TTL (seconds) for tokens minted via |
|
| pino log level ( |
| — |
|
How tiers, weights, and thresholds interact
A peer_review call is a weighted vote. The config decides who votes, in what order, and how many votes win:
weight— how much a source's agreement counts. Give larger weights to sources you trust more.tier— cost band. Tier 1 is consulted on every call; tier 2 only when tier 1 falls short; and so on. Put cheap, fast models in tier 1 and expensive frontier models above.thresholds— the agreeing weight required to declare quorum, keyed by target tier.
The one rule that is easy to miss:
A call has a single required weight: the threshold of its target tier. The target tier is the highest configured tier unless the caller caps escalation with the
tierinput. Lower tiers' thresholds are not applied along the way —thresholds.tier1only ever binds a call made with"tier": 1.
So with the Quick Start config ({"tier1": 2, "tier2": 4}), a default call must accumulate agreeing weight 4 (tier 2's threshold). Escalation is lazy, not mandatory: if tier 1's agreeing weight alone reaches 4, tier 2 is never contacted.
Execution order for one call:
All sources of the lowest tier are queried in parallel.
As soon as the responses in hand could arithmetically meet the required weight, the arbiter (at temperature 0) rates each response 0–1 against its own consensus answer. A source agrees when its rating is ≥ 0.7; only agreeing sources' weights count.
If agreeing weight ≥ required weight, quorum is achieved: any still-in-flight requests are aborted (cost saved) and the arbiter's consensus answer is returned.
Otherwise the next tier fans out and the arbiter re-rates over all accumulated responses — earlier answers are never discarded.
Tiers that were never reached do not appear in
quorum.sources[]at all.
Sizing guidance:
Overprovision each tier. A tier whose potential weight exactly equals the threshold escalates the moment any one source fails or dissents. Three weight-1 sources against a threshold of 2 tolerate one outlier; two sources tolerate none.
Prefer model diversity within a tier. Two copies of the same model mostly vote together, so their combined weight overstates the independence of the "second opinion".
Keep the required weight reachable. The target tier's threshold must be ≤ the summed weight of all sources in tiers ≤ target, or
quorum.achievedcan never be true.Pick a cheap, reliable arbiter. The arbiter is called at least once per
peer_reviewcall plus once per escalation, and its tokens count towardtokenUsage. It can double as a regular peer. If the arbiter's own call fails, the whole evaluation falls back toarbiterFailed: truewithcertaintyScore: 0— so give the arbiter role to the source with your most dependable credential.
Example: a three-tier stack
The walkthroughs below all use this config — two cheap flash-class voters in tier 1, a mid-price model in tier 2, a frontier model in tier 3:
{
"thresholds": { "tier1": 2, "tier2": 3, "tier3": 4 },
"arbiter": "gpt-mini",
"sources": [
{ "name": "gemini-flash", "apiType": "openai", "baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai", "model": "gemini-3-flash-preview", "apiKeyEnv": "GEMINI_API_KEY", "weight": 1, "tier": 1 },
{ "name": "gpt-mini", "apiType": "openai", "baseUrl": "https://api.openai.com/v1", "model": "gpt-5-mini", "apiKeyEnv": "OPENAI_API_KEY", "weight": 1, "tier": 1 },
{ "name": "gemini-pro", "apiType": "openai", "baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai", "model": "gemini-3-pro", "apiKeyEnv": "GEMINI_API_KEY", "weight": 2, "tier": 2 },
{ "name": "claude", "apiType": "anthropic", "baseUrl": "https://api.anthropic.com", "model": "claude-fable-5", "apiKeyEnv": "ANTHROPIC_API_KEY", "weight": 2, "tier": 3 }
]
}A default call targets tier 3, so it needs agreeing weight 4. A "tier": 1 call needs only 2. Note the arbiter is gpt-mini, deliberately not one of the Gemini sources: if GEMINI_API_KEY ever goes missing, evaluation still works (see scenario 4).
What happens when a query comes in
1. Easy factual query — quorum at tier 2, tier 3 never billed
{ "prompt": "What does HTTP status 431 mean? One sentence." }
Tier 1 fans out to gemini-flash and gpt-mini; both agree, but their combined weight (2) can't reach the required 4, so tier 2 is consulted. gemini-pro agrees too: 1 + 1 + 2 = 4 ≥ 4 — quorum. claude is never called and never appears in sources[].
// data
{
"response": "HTTP 431 (Request Header Fields Too Large) means the server refused the request because its headers are too big.",
"certaintyScore": 0.98, // min(1, 4/4) × mean(1.0, 1.0, 0.95), rounded
"quorum": {
"achieved": true,
"tier": 2, // tier reached; requiredWeight stays the target tier's threshold
"requiredWeight": 4,
"agreeingWeight": 4,
"sources": [
{ "name": "gemini-flash", "model": "gemini-3-flash-preview", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gpt-mini", "model": "gpt-5-mini", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gemini-pro", "model": "gemini-3-pro", "status": "ok", "weight": 2, "agreement": 0.95 }
]
},
"tokenUsage": { "prompt": 812, "completion": 118, "total": 930 } // includes both arbiter rounds
}2. Low-stakes check — cap escalation with "tier": 1
{ "prompt": "Is 'reciept' spelled correctly?", "tier": 1 }
Now the required weight is tier 1's threshold (2). Both flash sources agree, quorum is achieved in one round, and tiers 2–3 are never contacted regardless of the outcome. This is the cheap path for queries where two small models agreeing is good enough.
// data
{
"response": "No — the correct spelling is \"receipt\".",
"certaintyScore": 0.95, // min(1, 2/2) × mean(1.0, 0.9)
"quorum": {
"achieved": true,
"tier": 1,
"requiredWeight": 2,
"agreeingWeight": 2,
"sources": [
{ "name": "gemini-flash", "model": "gemini-3-flash-preview", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gpt-mini", "model": "gpt-5-mini", "status": "ok", "weight": 1, "agreement": 0.9 }
]
},
"tokenUsage": { "prompt": 296, "completion": 41, "total": 337 }
}3. Contested question — an outlier forces the full stack
{ "prompt": "In Node 20, is structuredClone faster than a JSON round-trip for small flat objects?" }
gemini-flash answers the opposite of everyone else and the arbiter rates it 0.2 — below the 0.7 agreement bar, so its weight is excluded (but still itemized). Agreeing weight climbs 1 → 3 → 5 as tiers 2 and 3 are pulled in, with the arbiter re-rating the accumulated set each round; quorum lands at tier 3.
// data
{
"response": "For small flat objects a JSON round-trip is usually still faster in Node 20; structuredClone wins once values contain types JSON can't represent.",
"certaintyScore": 0.9, // min(1, 5/4) × mean(0.95, 0.9, 0.85) — outlier's 0.2 not averaged in
"quorum": {
"achieved": true,
"tier": 3,
"requiredWeight": 4,
"agreeingWeight": 5, // gpt-mini 1 + gemini-pro 2 + claude 2; gemini-flash excluded
"sources": [
{ "name": "gemini-flash", "model": "gemini-3-flash-preview", "status": "ok", "weight": 1, "agreement": 0.2 },
{ "name": "gpt-mini", "model": "gpt-5-mini", "status": "ok", "weight": 1, "agreement": 0.95 },
{ "name": "gemini-pro", "model": "gemini-3-pro", "status": "ok", "weight": 2, "agreement": 0.9 },
{ "name": "claude", "model": "claude-fable-5", "status": "ok", "weight": 2, "agreement": 0.85 }
]
},
"tokenUsage": { "prompt": 2140, "completion": 486, "total": 2626 }
}4. Degraded run — missing credential, quorum unreachable
Same easy prompt as scenario 1, but GEMINI_API_KEY is unset, taking out both gemini-flash and gemini-pro. The remaining potential weight (gpt-mini 1 + claude 2 = 3) can never reach 4, so the call completes with quorum.achieved: false — still success: true, never an error. The degradation is itemized, not hidden.
// data
{
"response": "HTTP 431 (Request Header Fields Too Large) means the server refused the request because its headers are too big.",
"certaintyScore": 0.73, // min(1, 3/4) × mean(1.0, 0.95), rounded — shortfall caps the score
"quorum": {
"achieved": false,
"tier": 3,
"requiredWeight": 4,
"agreeingWeight": 3,
"sources": [
{ "name": "gemini-flash", "model": "gemini-3-flash-preview", "status": "unavailable", "weight": 1, "agreement": null },
{ "name": "gpt-mini", "model": "gpt-5-mini", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gemini-pro", "model": "gemini-3-pro", "status": "unavailable", "weight": 2, "agreement": null },
{ "name": "claude", "model": "claude-fable-5", "status": "ok", "weight": 2, "agreement": 0.95 }
]
},
"tokenUsage": { "prompt": 640, "completion": 97, "total": 737 }
}Had the arbiter itself been the casualty (e.g. OPENAI_API_KEY missing in this config), no agreement could be evaluated at all and the response would instead carry the highest-weighted successful answer with "certaintyScore": 0 and "quorum": { "arbiterFailed": true, ... }.
5. Checking your own answer — callerAnswer the peers disagree with
{ "prompt": "In JavaScript, does [10, 9, 100].sort() return [9, 10, 100]?", "callerAnswer": "Yes, sort() orders numbers ascending by default." }
You already have an answer and want independent endorsement. The peers never see callerAnswer — they answer the prompt blind. Tier 1's two flash voters agree with each other but can't reach weight 4, so tier 2 is pulled in; gemini-pro agrees too and quorum lands at tier 2 (1 + 1 + 2 = 4). The arbiter separately rates your answer against the peer consensus and returns callerAgreement — low here, because default sort() is lexicographic ([10, 100, 9]), so your answer contradicts what the peers concluded.
// data
{
"response": "No — Array.prototype.sort() coerces elements to strings by default, so [10, 9, 100].sort() returns [10, 100, 9]. Pass a comparator, e.g. .sort((a, b) => a - b), for numeric order.",
"certaintyScore": 0.98, // peers-only: min(1, 4/4) × mean(1.0, 1.0, 0.95) — unaffected by callerAnswer
"callerAgreement": 0.1, // arbiter rated your answer against the peer consensus; it contradicts it
"quorum": {
"achieved": true, // the peers reached quorum with each other; your answer carries no weight
"tier": 2,
"requiredWeight": 4,
"agreeingWeight": 4,
"sources": [
{ "name": "gemini-flash", "model": "gemini-3-flash-preview", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gpt-mini", "model": "gpt-5-mini", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gemini-pro", "model": "gemini-3-pro", "status": "ok", "weight": 2, "agreement": 0.95 }
]
},
"tokenUsage": { "prompt": 934, "completion": 141, "total": 1075 }
}quorum.achieved: true with callerAgreement: 0.1 is the signal to catch: the peers are confident, but not with you — treat it as a flag to revise your answer. Had you been right, callerAgreement would sit near 1 alongside the same high certaintyScore. Note caller never appears in sources[] and its rating never moved agreeingWeight.
Reading the result: gate any automated decision on quorum.achieved, treat certaintyScore as the confidence dial (it discounts both shortfall and lukewarm agreement), scan sources[] for error/unavailable entries — a degraded quorum is always visible, never silent — and, when you passed a callerAnswer, read callerAgreement as the separate "did the peers land where I did?" signal (scenario 5).
Tools
All tools return the envelope {"success": true, "data": ...} or {"success": false, "error": {"code", "message"}}, JSON-stringified into a single text content block (compatible with the reference Gemini MCP). Error codes: VALIDATION_ERROR, CONFIGURATION_ERROR, TIMEOUT_ERROR, EXTERNAL_SERVICE_ERROR, PEER_API_ERROR, PEER_RATE_LIMIT, TOOL_NOT_FOUND, INTERNAL_ERROR. Model, temperature, and output limits are injected server-side — the input schemas are strict, so client-supplied model/temperature are rejected.
peer_review
Weighted-quorum consultation. Input: prompt (1–100000 chars), optional history ([{role: "user"|"model", content}]), optional tier (highest tier to escalate to; defaults to the full stack), optional callerAnswer (1–100000 chars — your own answer to the prompt). An unmet quorum is not an error — check quorum.achieved and certaintyScore.
// request
{ "prompt": "What is the capital of France? Answer with just the city name." }
// response data
{
"response": "Paris",
"certaintyScore": 1, // min(1, agreeingWeight/requiredWeight) × mean agreement of agreeing sources
"quorum": {
"achieved": true,
"tier": 1,
"requiredWeight": 2,
"agreeingWeight": 2,
"sources": [
{ "name": "gemini-a", "model": "google/gemini-3.5-flash", "status": "ok", "weight": 1, "agreement": 1 },
{ "name": "gemini-b", "model": "google/gemini-3.5-flash", "status": "ok", "weight": 1, "agreement": 1 }
]
},
"tokenUsage": { "prompt": 315, "completion": 45, "total": 360 } // aggregated across peers + arbiter
}sources[].status is ok, error, or unavailable; agreement is the arbiter's 0–1 rating (null if the source produced no rated response). If the arbiter itself fails, the response falls back to the highest-weighted successful answer with certaintyScore: 0 and quorum.arbiterFailed: true.
Including your own answer (callerAnswer). When you already have an answer and want to know whether independent peers endorse it, pass it as callerAnswer instead of pasting it into prompt. Peers never see it — they answer the prompt blind, so their independence is preserved and certaintyScore stays an anchoring-free signal. The arbiter is instructed to derive the consensus only from the peer answers and then rate your answer against that consensus, so a confidently-wrong callerAnswer cannot pull the consensus toward itself. The response gains a callerAgreement field (0–1).
How it behaves:
Zero quorum weight.
callerAgreementis reported alongside the quorum but never counted intoagreeingWeight,quorum.achieved, orcertaintyScore— your own answer cannot self-certify. Read it as a separate signal: "did independent peers land where I did?"Present only when supplied. The key is omitted entirely from the response when
callerAnswerwas not passed. When it was passed, the key is always present.nullwhen unrated.callerAgreementisnullif the arbiter returned no rating for it — either because the arbiter omitted thecallerentry from its ratings, or because there was no trusted evaluation at all (arbiter failed, or quorum ran with no ratable peer response — the same fallback paths that forcecertaintyScore: 0). A suppliedcallerAnswertherefore always yields the key, but not always a number.Reserved name.
calleris reserved as the internal rating channel and is rejected as a configuredsources[].nameat startup.
// request
{ "prompt": "What is the capital of France?", "callerAnswer": "Paris" }
// response data — same shape as a normal peer_review, plus callerAgreement:
{
"response": "Paris",
"certaintyScore": 1, // unchanged by callerAnswer — derived from peers only
"callerAgreement": 1, // arbiter's 0–1 rating of "Paris" vs the peer consensus
"quorum": { "achieved": true, "requiredWeight": 2, "agreeingWeight": 2, /* … */ },
"tokenUsage": { "prompt": 315, "completion": 45, "total": 360 }
}When your answer diverges from what the peers conclude, callerAgreement drops toward 0 while certaintyScore still reflects the peers' own convergence — e.g. { "callerAgreement": 0.15, "certaintyScore": 0.97 } reads as "the peers strongly agree, but not with you."
query_peer
Single-source escape hatch mirroring the reference query_gemini. Input: prompt, optional history, optional source (defaults to the lowest-tier highest-weight source).
// request
{ "prompt": "In one word, what color is the sky on a clear day?", "source": "gemini" }
// response data
{ "response": "Blue", "model": "google/gemini-3.5-flash", "source": "gemini", "finishReason": "stop",
"tokenUsage": { "prompt": 14, "completion": 1, "total": 15 } }list_peers
No input. Returns every configured source with availability (available = credential resolvable):
{ "sources": [ { "name": "gpt-large", "apiType": "openai", "model": "gpt-5.2", "weight": 2, "tier": 1, "available": true } ], "count": 1 }count_tokens
Input: text (1–1000000 chars), optional source. Anthropic-compatible sources use the provider's count endpoint ("method": "api"); OpenAI-compatible sources use a ceil(chars/4) heuristic ("method": "estimate").
{ "totalTokens": 42, "model": "claude-fable-5", "source": "claude", "method": "api" }Migrating from gemini-for-claude-mcp
This server fully supersedes gemini-for-claude-mcp: Gemini becomes just one configured source, reachable via AI Studio or Vertex AI.
It is intended as a drop-in replacement: query_peer accepts the same prompt/history shape (role: "user"|"model" preserved) and returns the same payload fields as query_gemini plus source; list_peers covers list_gemini_models; count_tokens covers count_gemini_tokens (with the method caveat below). Configure Gemini as an ordinary source, either way:
AI Studio (static key):
{ "name": "gemini", "apiType": "openai",
"baseUrl": "https://generativelanguage.googleapis.com/v1beta/openai",
"model": "gemini-3-pro", "apiKeyEnv": "GEMINI_API_KEY", "weight": 2, "tier": 1 }Vertex AI (short-lived OAuth token via gcloud):
{ "name": "gemini", "apiType": "openai",
"baseUrl": "https://aiplatform.googleapis.com/v1beta1/projects/YOUR_PROJECT/locations/global/endpoints/openapi",
"model": "google/gemini-3.5-flash",
"apiKeyCommand": "gcloud auth print-access-token", "weight": 2, "tier": 1 }Caveat: count_tokens for Gemini uses the ceil(chars/4) estimate ("method": "estimate") rather than Gemini's native count endpoint.
Development
npm run typecheck && npm run lint && npm test # full suite
npm run dev # tsx watch modeSee ARCHITECTURE.md for layer rules, the quorum algorithm, and runtime guards.
Contributing
Issues and pull requests are welcome. Before submitting a PR, run the full check suite:
npm run typecheck && npm run lint && npm testLicense
Apache-2.0 — see LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- 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/raydollete/peer-review'
If you have feedback or need assistance with the MCP directory API, please join our Discord server