Audit — R93 B3 Document Dual Error-Envelope Pattern in s17

Round: R93 debug-sweep (fix #3 of 6, doc-only) Branch: feature/r93-b3-s17-dual-envelope Base SHA: fbc8808a β task: 1a267787-b864-4f5f-a7dd-669a6268dbe7

§1. Goal

Document the two error-reporting contracts that coexist on the Phase 0 + R89A + R89B + R92 MCP surface. The current docs/spec/s17-mcp-surface.md §6 Response shape describes only one error envelope ({ok:false, error:{code,message,details?}}), but the live surface uses two distinct patterns. A client written against the spec as it stands will mis-parse half the surface.

§2. The two patterns observed

Pattern A — throw → α middleware envelope. Handler throws; the α middleware Stage 4 catch (src/server.ts:362-375) wraps the error as:

{ "ok": false, "error": { "code": "HANDLER_ERROR", "message": "<err.message>" } }

Or for Zod input failure (Stage 2, src/server.ts:309-336):

{ "ok": false, "error": { "code": "INVALID_PARAMS", "message": "...", "details": { "issues": [...] } } }

The structuredContent field at the JSON-RPC layer is the SAME envelope (isError: true flag set).

Pattern B — return error envelope as data. Handler returns a structured error value (no throw); middleware Stage 4 wraps it as success-with-data:

{ "ok": true, "data": { "ok": false, "error": { "code": "<DOMAIN_CODE>", "message": "..." } } }

structuredContent.ok === true; the actual error is at structuredContent.data.ok.

This pattern is explicitly documented in code at src/tools/merkle.ts:392-405:

“Envelope returned by handlers on domain error. Pattern matches the task-tool handlers (src/domains/tasks/repository.ts:700-765) — success returns the data payload directly (α middleware wraps it), failure returns a structured {ok:false, error:{...}} as the data payload so the client can read envelope.data.ok.”

§3. Per-tool inventory (27 tools)

Verified at fbc8808a via grep + live MCP probes during the R93 debug audit:

Pattern A (throw → middleware envelope) — 17 tools

Tool Source Error codes
server_ping src/server.ts:541-554 INVALID_PARAMS only
server_health src/tools/health.ts INVALID_PARAMS only
thought_record src/domains/trail/repository.ts:404-420 INVALID_PARAMS, HANDLER_ERROR (e.g. SQLite UNIQUE violation)
thought_record_list src/domains/trail/repository.ts:422-441 INVALID_PARAMS, HANDLER_ERROR
audit_verify_chain src/domains/trail/verifier.ts:174-202 INVALID_PARAMS, HANDLER_ERROR
skill_list src/domains/skills/repository.ts INVALID_PARAMS, HANDLER_ERROR
reputation_get src/domains/reputation/tools.ts INVALID_PARAMS, HANDLER_ERROR
reputation_history src/domains/reputation/tools.ts INVALID_PARAMS, HANDLER_ERROR
reputation_leaderboard src/domains/reputation/tools.ts INVALID_PARAMS, HANDLER_ERROR
reputation_check_gates src/domains/reputation/tools.ts INVALID_PARAMS, HANDLER_ERROR
consensus_propose src/domains/consensus/tools.ts:552-563 INVALID_PARAMS, HANDLER_ERROR (ROUND_NOT_FOUND, ALREADY_VOTED)
consensus_vote src/domains/consensus/tools.ts:565-576 INVALID_PARAMS, HANDLER_ERROR (ROUND_NOT_FOUND, ALREADY_VOTED)
consensus_finality src/domains/consensus/tools.ts:578-589 INVALID_PARAMS, HANDLER_ERROR (ROUND_NOT_FOUND)
consensus_gossip src/domains/consensus/tools.ts:591-602 INVALID_PARAMS only (Phase 0 always returns empty arrays)
vrf_eval src/domains/consensus/tools.ts:604-615 INVALID_PARAMS, HANDLER_ERROR (INVALID_KEY)
router_score src/domains/router/tools.ts:434-445 INVALID_PARAMS, HANDLER_ERROR
router_call src/domains/router/tools.ts:447-458 INVALID_PARAMS, HANDLER_ERROR (FallbackChainExhaustedError)
router_fallback src/domains/router/tools.ts:460-471 INVALID_PARAMS, HANDLER_ERROR
router_stats src/domains/router/tools.ts:473-484 INVALID_PARAMS only

Wait, that’s 19 not 17. Let me recount the table — yes 19 entries above.

Pattern B (handler-returned error envelope as data) — 8 tools

Tool Source Domain error codes returned in data.error.code
task_create src/domains/tasks/repository.ts:~700-720 ERR_VALIDATION (per Phase 0 task contract)
task_get src/domains/tasks/repository.ts:~723-748 ERR_NOT_FOUND
task_update src/domains/tasks/repository.ts:475 + 720+ ERR_NOT_FOUND, ERR_WRITEBACK_REQUIRED, ERR_INVALID_TRANSITION
task_list src/domains/tasks/repository.ts (no observed domain errors — returns data)
task_next_actions src/domains/tasks/repository.ts (no observed domain errors — returns data)
audit_session_start src/tools/merkle.ts:417-444 ERR_SESSION_EXISTS
merkle_finalize src/tools/merkle.ts:446-493 ERR_SESSION_NOT_FOUND, ERR_ALREADY_FINALIZED, ERR_NO_RECORDS
merkle_root src/tools/merkle.ts:495-519 ERR_NOT_FINALIZED

That totals to 19 + 8 = 27 ✓.

task_list and task_next_actions are technically in the same handler-block as the other task tools and would return Pattern B envelopes if they had failure paths — they currently don’t observably emit errors from the handler.

§4. Live reproduction (the inconsistency)

task_get {id:"00000000-…"}          → {ok:true,  data:{ok:false, error:{code:"ERR_NOT_FOUND",…}}}
merkle_root {session_id:"missing"}  → {ok:true,  data:{ok:false, error:{code:"ERR_NOT_FINALIZED",…}}}
consensus_finality {round_id:"bad"} → {ok:false, error:{code:"INVALID_PARAMS",…}}     (regex reject — Pattern A)
vrf_eval {priv_key_hex:""}          → {ok:false, error:{code:"HANDLER_ERROR", message:"INVALID_KEY: ..."}}

A naïve client that checks response.structuredContent.ok will:

  • Correctly detect failure on vrf_eval (Pattern A).
  • See ok:true on merkle_root even when the session does not exist — and miss the error unless it also reaches into data.ok.

§5. Why both patterns exist

  • Pattern A (throw) is the JSON-RPC norm and lines up with how the α middleware was originally designed (src/server.ts:362-375).
  • Pattern B (return error envelope as data) was chosen for task_* and merkle_* because domain-specific error codes (ERR_NOT_FOUND, ERR_NOT_FINALIZED, ERR_WRITEBACK_REQUIRED, etc.) deserved a structured response without conflating with transport-level HANDLER_ERROR. The explicit choice is documented at src/tools/merkle.ts:392-405.

The dichotomy is INTENTIONAL but currently undocumented. Clients reading s17-mcp-surface.md §6 alone cannot predict which path a tool takes.

§6. Constraints on the fix

  • MUST NOT modify any handler behaviour. Pattern preservation is the entire premise.
  • MUST update docs/spec/s17-mcp-surface.md §6 to describe both patterns.
  • MUST cite the source comment at src/tools/merkle.ts:392-405 as the canonical Pattern B definition.
  • MAY link to the per-tool catalogue (docs/reference/mcp-tools-phase-0.md) for the exhaustive per-tool table — but the catalogue currently only covers the 14 Phase 0 tools; out of scope to extend it to 27 in this slice.
  • SHOULD include at least one example of each pattern at the wire-level so client developers can dispatch on the right shape.

§7. Path forward

Edit docs/spec/s17-mcp-surface.md §6 (lines 77-91). Replace the single error-envelope block with two subsections: §6.1 success envelope + §6.2 dual failure envelope. Append a §6.3 “Which pattern does which tool family use” mini-table that maps tool families to patterns and references the source comment at src/tools/merkle.ts:392-405.

Proceeding to contract.


Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.