Survey Review MCP#
survey-review-mcp is a Model Context Protocol
server that exposes a review-queue session for inspection and decision-making
through an MCP client.
The installed CLI uses the official MCP TypeScript server runtime over stdio. It does not make network requests. All review state lives in one local session JSON file.
Protocol compatibility#
One server definition serves both MCP eras:
- MCP 2026-07-28 clients open with
server/discover. - Existing clients continue to use the legacy
initializeflow.
There is no protocol setting to manage. The opening request selects the compatible era automatically, while the same queue, decision validation, and atomic persistence code runs behind both. Modern cacheable discovery, list, and resource responses are private with a zero TTL because the session is mutable.
Start the server#
npx survey-review-mcp --session path/to/session.json
Options:
| Flag | Description |
|---|---|
--session <path> |
Path to the session JSON file (read and updated in-place). Defaults to the bundled example-data/mcp-review-session.json fixture. |
--no-ui |
Suppress the embedded UI resource from survey_review_queue and survey_review_item results. Text content is always returned. |
Tool table#
| Tool | Args | Returns |
|---|---|---|
survey_review_queue |
(none) | Text summary + JSON of all items with status, active item, queue progress, and session totals |
survey_review_item |
itemName: string |
Full item detail: current vs proposed values, confidence, source refs, excerpts, and current decision |
survey_review_decide |
itemName: string, decision: "accept" | "hold" | "reject" | "could-not-confirm" | "select", note?: string, reason?: string, attemptEvidenceIds?: string[], candidateId?: string |
Applies the decision through the real session APIs and persists atomically. reason is required and non-empty for could-not-confirm. candidateId is required for select. |
Decision mapping:
| MCP value | ReviewWorkbenchDecision | Effect |
|---|---|---|
accept |
accept-proposed |
Proposed value becomes the verified outcome |
hold |
keep-current |
Current value remains the verified outcome |
reject |
reject-proposed |
Proposed value is rejected; current value is unmodified |
could-not-confirm |
could-not-confirm |
Review round ends without changing, rejecting, or escalating the claim; a reason is required |
select |
select-proposed |
On a conflict (several proposed values), the value named by candidateId becomes the verified outcome; the other proposed values are recorded as seen and not chosen |
On a conflict item, survey_review_item lists each proposed value with its
candidateId, and the card offers "Use this value" per value. select is
refused for an id that is not one of the item's proposed candidates, and for an
item with a single proposed value (use accept). accept stays refused on a
conflict. After a choice the item text, data (selectedCandidateId, and
chosen on each proposed candidate) and card mark every value chosen or not
chosen.
Rejected calls#
Domain errors such as an unknown item or an item that is already decided return
{ isError: true, content: [{ type: "text", text: "..." }] }. Schema-invalid
arguments, including an unknown decision value, are rejected by the official
runtime before the handler runs. In every negotiated protocol path the server
rejects these calls before applying or persisting a decision.
UI card behavior#
survey_review_queue and survey_review_item include an embedded UI resource:
{
"type": "resource",
"resource": {
"uri": "ui://survey/review-card/<instance>",
"mimeType": "text/html;profile=mcp-app",
"text": "<!doctype html>...",
"_meta": {
"ui": {
"csp": {
"connectDomains": [],
"resourceDomains": []
}
},
"mcpui.dev/ui-preferred-frame-size": ["420px", "560px"]
}
}
}
The HTML card is compact and fully self-contained (no external network requests). It shows:
- Current vs proposed values side by side
- Confidence percentage and source reference for each candidate
- Excerpt from the source locator
- Queue progress (resolved / total)
- Current decision badge (if any)
- A reviewer note textarea
- Accept / Hold / Reject / Could not confirm buttons
Buttons post back to the host using the MCP guest-to-host postMessage protocol:
window.parent.postMessage({
jsonrpc: "2.0",
id: <n>,
method: "tools/call",
params: { name: "survey_review_decide", arguments: { itemName, decision, note } }
}, "*");
The card uses --k-* design tokens matching the Console Kit dark theme by default, with prefers-color-scheme: light support. No fonts or assets are fetched from the network.
Pass --no-ui to suppress the UI resource; the text content items are always returned.
The queue tool also declares its app using canonical nested
_meta.ui.resourceUri. The flat _meta["ui/resourceUri"] key remains
transitional compatibility output for older Apps hosts. New integrations should
read the nested shape.
Session file contract#
The session file is a JSON object with three keys, and an optional fourth:
{
"session": { ... }, // ReviewSession resource (metadata only)
"snapshot": { ... }, // ReviewQueueSessionState at session creation
"events": [ ... ], // ReviewSessionEvent array (appended by decide)
"extractionImport": { ... } // optional: the ExtractionEnvelopeImport record the snapshot's items came from
}
When extractionImport is present, every tool checks the snapshot's items
against it (attestReviewQueueExtraction). A queue that does not match it (for
example an excluded rival value deleted from a stored item) is refused: every
tool returns isError and nothing is written. When it is absent and the items
came from an extraction import, the queue is served with an "Unverified queue"
notice at the top of the queue and item text and on the card, and the data
carries queueAttestation: "unverified". Store the import record with the
session when you create it.
The snapshot field is the baseline state. All decisions are recorded as append-only ReviewSessionEvent records in events. When a tool reads state it replays the events over the snapshot using replayReviewSessionEvents. When survey_review_decide applies a new decision it:
- Replays events to derive the current live state.
- Builds the updated session with the new decision.
- Generates a fresh full event stream via
buildReviewSessionEvents. - Validates the new events using
deriveServerReviewSessionApplyResult(freshness + replay checks). - Writes the updated file atomically (temp file + rename).
The file is written atomically so a failed write leaves the previous state intact.
No-network statement#
The MCP server makes no outbound network requests. It reads and writes one local
file. The embedded HTML card contains no external <script>, <link>, <img>,
or @import references, and its Apps CSP declares no connect or resource
domains. Standard output is reserved for JSON-RPC; sanitized SDK and transport
diagnostics are written only to standard error.