P3.7.1 — θ MCP Tool Surface — Execution Packet

Round: R89 θ Wave 4 Branch: feature/p3-7-1-mcp-tools Worktree: .worktrees/claude/p3-7-1-mcp-tools Step: 3 of 5 Audit: 2e1cc953 · Contract: d6943993 Date: 2026-05-13


§1. Execution plan (file-level)

§1.1 Step 4 — Implement

Order File Action Approx LoC
1 src/domains/consensus/tools.ts CREATE — 5 tool registrations + handlers + Zod schemas ~400
2 src/server.ts EDIT — add import { registerConsensusTools } from './domains/consensus/tools.js'; and a registerConsensusTools(ctx) call inside bootstrap() +6
3 src/__tests__/domains/consensus/tools.test.ts CREATE — 21 tests across 6 describe blocks ~500

§1.2 Step 5 — Verify

Order Command Expected
1 npm run build Exit 0, no TS errors
2 npm run lint Exit 0, no ESLint errors
3 npm test -- --testPathPattern=consensus/tools All 21 new tests pass
4 npm test Baseline + 21 = ~2991 passing
5 gh pr create with body summary + writeback PR opened

§2. Implementation order (tools.ts internals)

  1. JSDoc header (canonical references, surface enumeration, forbidden-token policy).
  2. Imports (z, KeyObject from crypto, signMessage + Vote from messages, FinalitySM + FinalityLevel from finality, voteGroupKey from quorum, vrfEval + VrfError from vrf-stub, registerColibriTool + ColibriServerContext from server).
  3. Internal types — RoundEntry.
  4. Module-singleton state — __roundCounter, __rounds, __keyPair, plus a lazy __getKeyPair() accessor.
  5. resetConsensusToolsForTesting() — clears all 4 singletons (keyPair too, to keep tests deterministic).
  6. Zod input schemas — 5.
  7. Zod output schemas — 5.
  8. Type exports — derived via z.infer.
  9. Internal helpers:
    • __lookupRound(round_id_str): RoundEntry (throws ROUND_NOT_FOUND).
    • __nextRoundId(): bigint (counter++).
    • __getKeyPair(): { priv: KeyObject; pub: KeyObject; arbiter_id: string } (lazy generate; arbiter_id is "node-0").
  10. Handlers (5) — pure functions taking parsed input, throwing typed errors:
    • consensusPropose(input): ConsensusProposeOutput
    • consensusVote(input): ConsensusVoteOutput
    • consensusFinality(input): ConsensusFinalityOutput
    • consensusGossip(input): ConsensusGossipOutput
    • vrfEvalHandler(input): VrfEvalOutputShape
  11. registerConsensusTools(ctx) — 5 registerColibriTool calls.

§3. JSDoc + comments — required content

Top-of-file JSDoc must cite (per the canonical references convention used in messages.ts, reputation/tools.ts):

  • docs/audits/p3-7-1-mcp-tools-audit.md
  • docs/contracts/p3-7-1-mcp-tools-contract.md
  • docs/packets/p3-7-1-mcp-tools-packet.md
  • docs/guides/implementation/task-prompts/p3.1-theta-consensus.md §P3.7.1
  • docs/3-world/physics/laws/consensus.md §Phase 0 posture
  • src/server.ts — α 5-stage middleware
  • src/domains/consensus/{messages,quorum,finality,vrf-stub}.ts — upstream surfaces

Per-handler JSDoc must document inputs, outputs, error modes, single-arbiter posture, and process-singleton state interaction.


§4. Concrete Zod schemas — locked

// Hex regexes — locked.
const HEX_64 = /^[0-9a-f]{64}$/;
const HEX_EVEN_LEN_NONEMPTY = /^([0-9a-f]{2})+$/;
const DECIMAL_POSITIVE = /^[1-9][0-9]*$/;

// consensus_propose
export const ConsensusProposeInputSchema = z.object({
  event: z.object({
    merkle_root_hex: z.string().regex(HEX_64),
    rule_version_hash_hex: z.string().regex(HEX_64),
  }).strict(),
}).strict();

export const ConsensusProposeOutputSchema = z.object({
  round_id: z.string().regex(DECIMAL_POSITIVE),
  status: z.enum(['PENDING', 'QUORUM']),
}).strict();

// consensus_vote
export const ConsensusVoteInputSchema = z.object({
  round_id: z.string().regex(DECIMAL_POSITIVE),
  vote: z.object({
    merkle_root_hex: z.string().regex(HEX_64),
    rule_version_hash_hex: z.string().regex(HEX_64),
    vote_type: z.enum(['ACCEPT', 'REJECT', 'ABSTAIN']),
  }).strict(),
  privKey: z.string().regex(HEX_EVEN_LEN_NONEMPTY).optional(),
}).strict();

export const ConsensusVoteOutputSchema = z.object({
  vote_signed: z.literal(true),
  sig_b64: z.string(),
}).strict();

// consensus_finality
export const ConsensusFinalityInputSchema = z.object({
  round_id: z.string().regex(DECIMAL_POSITIVE),
}).strict();

export const ConsensusFinalityOutputSchema = z.object({
  round_id: z.string().regex(DECIMAL_POSITIVE),
  level: z.enum(['PENDING', 'SOFT', 'QUORUM', 'HARD', 'ABSOLUTE']),
  evidence: z.string().regex(/^([0-9a-f]{2})*$/).optional(),
}).strict();

// consensus_gossip
export const ConsensusGossipInputSchema = z.object({
  peer_id: z.string().min(1),
  events: z.array(z.string()).optional(),
}).strict();

export const ConsensusGossipOutputSchema = z.object({
  events_sent: z.array(z.string()),
  events_received: z.array(z.string()),
}).strict();

// vrf_eval
export const VrfEvalInputSchema = z.object({
  seed_hex: z.string().regex(HEX_EVEN_LEN_NONEMPTY),
  input_hex: z.string().regex(HEX_EVEN_LEN_NONEMPTY),
  priv_key_hex: z.string().regex(HEX_EVEN_LEN_NONEMPTY),
}).strict();

export const VrfEvalOutputSchema = z.object({
  output_hex: z.string().regex(HEX_64),
  proof_hex: z.string().regex(HEX_64),
}).strict();

§5. Test file structure — locked

// src/__tests__/domains/consensus/tools.test.ts

import { z } from 'zod';

import {
  ConsensusFinalityInputSchema,
  ConsensusFinalityOutputSchema,
  ConsensusGossipInputSchema,
  ConsensusGossipOutputSchema,
  ConsensusProposeInputSchema,
  ConsensusProposeOutputSchema,
  ConsensusVoteInputSchema,
  ConsensusVoteOutputSchema,
  VrfEvalInputSchema,
  VrfEvalOutputSchema,
  consensusFinality,
  consensusGossip,
  consensusPropose,
  consensusVote,
  registerConsensusTools,
  resetConsensusToolsForTesting,
  vrfEvalHandler,
} from '../../../domains/consensus/tools.js';
import { createServer } from '../../../server.js';

const ROOT_HEX = '0'.repeat(64);
const RULE_HEX = '1'.repeat(64);
const ROOT_HEX_B = '2'.repeat(64);

beforeEach(() => {
  resetConsensusToolsForTesting();
});

describe('consensus_propose', () => { /* 3 tests */ });
describe('consensus_vote', () => { /* 6 tests */ });
describe('consensus_finality', () => { /* 3 tests */ });
describe('consensus_gossip', () => { /* 3 tests */ });
describe('vrf_eval', () => { /* 3 tests */ });
describe('registerConsensusTools', () => { /* 2 tests */ });
describe('smoke', () => { /* 1 test — full chain */ });

// Total: 21 tests

§6. server.ts edit — locked diff

@@ src/server.ts ~line 51 @@
+import { registerConsensusTools } from './domains/consensus/tools.js';

@@ src/server.ts inside bootstrap() — after registerReputationTools(ctx); @@
     registerReputationTools(ctx);
+    // P3.7.1: register θ Consensus MCP tools — consensus_propose,
+    // consensus_vote, consensus_finality, consensus_gossip, vrf_eval.
+    // Phase 0 single-arbiter posture (n=1 trivial finalization).
+    // Closes R89 θ Wave 4 — takes the total MCP surface count from
+    // 18 → 23 (first θ MCP surface).
+    registerConsensusTools(ctx);
     await start(ctx);

§7. Risk register — execution-time concerns

# Risk Detection Mitigation
E1 generateKeyPairSync('ed25519') import path Build fail Import from node:crypto (same as messages.ts)
E2 FinalitySM does not move PENDING → SOFT in one receiveVote call Test fail Verified in finality.ts:374-378: the FSM does PENDING → SOFT → QUORUM in cascade inside one receiveVote (lines 374-403). Confirmed.
E3 voteGroupKey round_id mismatch (FSM uses bigint, key uses decimal string) Test fail voteGroupKey writes ${v.round_id.toString()} — same encoding regardless of caller
E4 Strict mode on consensus_gossip rejects events?: undefined Test fail Zod .optional() allows omission; explicit undefined may differ. Verify in tests. If fail, switch to z.array(z.string()).default([])
E5 priv_key_hex empty string regex ^([0-9a-f]{2})+$ already rejects empty Logic flaw Empty string fails the regex → Zod rejects with INVALID_PARAMS. The handler-level INVALID_KEY is for cases where the regex passed but the buffer is somehow zero-length (e.g. "00" which is fine and 1 byte — not empty). The INVALID_KEY path is preserved for VrfError re-wrapping.

§8. Commit plan

Commit Files Message template
1 (audit) docs/audits/p3-7-1-mcp-tools-audit.md audit(p3-7-1-mcp-tools): inventory θ MCP surface — DONE @ 2e1cc953
2 (contract) docs/contracts/p3-7-1-mcp-tools-contract.md contract(p3-7-1-mcp-tools): behavioral contract for 5 θ MCP tools — DONE @ d6943993
3 (packet) docs/packets/p3-7-1-mcp-tools-packet.md packet(p3-7-1-mcp-tools): execution plan — THIS COMMIT
4 (feat) src/domains/consensus/tools.ts, src/__tests__/domains/consensus/tools.test.ts, src/server.ts feat(p3-7-1-mcp-tools): 5 θ MCP tools — MCP surface 18→23 (R89 θ Wave 4)
5 (verify) docs/verification/p3-7-1-mcp-tools-verification.md verify(p3-7-1-mcp-tools): test evidence + gate results

Five commits total.


§9. Out of scope (defer to later slices)

  • DB persistence of rounds/votes — Phase 1.5+ (matches consensus.md §Phase 0 posture stating tables are “schema-ready” but not migrated in Phase 0).
  • Multi-arbiter rounds (n > 1) — Phase 1.5+.
  • Real equivocation detection across processes — Phase 1.5+ (P3.5.1’s equivocation slice already handles in-memory equivocation proofs; this slice is the MCP wrapper).
  • Real gossip wire (IHAVE/IWANT) — P3.3.x.
  • RFC 9381 ECVRF — vrf_eval currently wraps the HMAC stub; swap on ADR-002 acceptance.
  • Adding the corpus self-scan to tools.ts — the consensus self-scan test set today scans messages, quorum, finality, vrf-stub, time-anchors, gossip-wire, round-state, equivocation; adding tools.ts requires extending that test, which is out of scope and not required for the AC.

§10. Sign-off

Packet reviewed against:

  • Audit + contract (above)
  • Source prompt §P3.7.1 acceptance criteria + gotchas
  • Dispatch packet “ship” list + forbiddens

Step 4 (implement) gates on this packet.


Back to top

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

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