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)
- JSDoc header (canonical references, surface enumeration, forbidden-token policy).
- 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).
- Internal types —
RoundEntry. - Module-singleton state —
__roundCounter,__rounds,__keyPair, plus a lazy__getKeyPair()accessor. resetConsensusToolsForTesting()— clears all 4 singletons (keyPair too, to keep tests deterministic).- Zod input schemas — 5.
- Zod output schemas — 5.
- Type exports — derived via
z.infer. - Internal helpers:
__lookupRound(round_id_str): RoundEntry(throwsROUND_NOT_FOUND).__nextRoundId(): bigint(counter++).__getKeyPair(): { priv: KeyObject; pub: KeyObject; arbiter_id: string }(lazy generate; arbiter_id is"node-0").
- Handlers (5) — pure functions taking parsed input, throwing typed errors:
consensusPropose(input): ConsensusProposeOutputconsensusVote(input): ConsensusVoteOutputconsensusFinality(input): ConsensusFinalityOutputconsensusGossip(input): ConsensusGossipOutputvrfEvalHandler(input): VrfEvalOutputShape
registerConsensusTools(ctx)— 5registerColibriToolcalls.
§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.mddocs/contracts/p3-7-1-mcp-tools-contract.mddocs/packets/p3-7-1-mcp-tools-packet.mddocs/guides/implementation/task-prompts/p3.1-theta-consensus.md§P3.7.1docs/3-world/physics/laws/consensus.md§Phase 0 posturesrc/server.ts— α 5-stage middlewaresrc/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_evalcurrently wraps the HMAC stub; swap on ADR-002 acceptance. - Adding the corpus self-scan to
tools.ts— the consensus self-scan test set today scansmessages,quorum,finality,vrf-stub,time-anchors,gossip-wire,round-state,equivocation; addingtools.tsrequires 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.