P0-23 Review + Governance MCP Tools
P0-23 Review + Governance MCP Tools
Section titled “P0-23 Review + Governance MCP Tools”Status: DRAFT — 2026-04-21 (S180 WP3)
Source: DECISIONS §3.1 P0-23 (v4 expanded) + Liam decisions (Session 180): Call A = A1 faithful (4 tools from DECISIONS), Call B = B2 (add review_governance_item for governance review actions).
Size: 3h spec; ~1d impl (per SPEC-SEQUENCE line 265).
Gate: C-1 breaking change audit (additive only — see §7).
Unblocks: P0-7 AssignmentManager delete (WP5); P1-33 change-management
skill; P1-34 daily-briefing skill.
1. Problem
Section titled “1. Problem”Rachel’s governance + assignment workflows are web-UI only. Claude-surface parity is missing, which blocks:
- P0-7 —
AssignmentManager.tsxis slated for deletion once MCP can assign reviews. - P1-33
knowledge-hub:change-managementskill — requiresget_governance_queueand a way to action governance reviews via MCP (approve / request_changes / revert). - P1-34
knowledge-hub:daily-briefingskill — requiresget_review_queue+get_assignments_for_user.
The existing update_governance_status tool only handles publish/draft
transitions, not governance review actions (approve/request_changes/revert).
That’s a semantic gap between the tool name and what the skill dependencies
need.
Subagent investigation confirmed:
- 4 tools are net-new (
get_review_queue,get_assignments_for_user,create_review_assignment,get_governance_queue) - 1 tool is net-new (
review_governance_item) to close the governance-review-action gap — distinct from the existingupdate_governance_statuswhich stays unchanged. - All 5 wrap existing API routes. Total tool count: 47 → 52.
2. Goals / Non-goals
Section titled “2. Goals / Non-goals”- Expose 5 new MCP tools via
defineToolso Claude-surface workflows have parity with the web UI for review and governance. - Each tool wraps an existing API route (thin wrappers — zero net-new query logic).
- Proper role gating: writes require admin/editor; admin-only for assignment creation.
- Response text is structured for LLM relay (markdown +
toStructuredContent). - C-1 breaking change audit: no existing tool, prompt, or resource modified.
- Tool registration order is stable and grouped (governance tools stay together, review tools stay together).
Non-goals
Section titled “Non-goals”- No renaming or semantic change to existing
update_governance_status(its publish/draft behaviour is preserved). - No PATCH review assignment tool (completion flow stays in web UI for now —
covered by
update_governance_statussemantically-adjacent tool for publish; assignment completion is a rarer flow). - No new API routes.
- No schema changes (all 5 reuse existing Zod schemas from
lib/validation/schemas.ts).
3. Design
Section titled “3. Design”3.1 Tool summary
Section titled “3.1 Tool summary”| # | Tool | Category file | Route wrapped | Annotations | Role |
|---|---|---|---|---|---|
| 1 | get_governance_queue | governance.ts (extend) | GET /api/governance/review | READ_ONLY_ANNOTATIONS | editor+ |
| 2 | review_governance_item | governance.ts (extend) | POST /api/governance/review | NON_IDEMPOTENT_WRITE_ANNOTATIONS | editor+ |
| 3 | get_review_queue | review.ts (new) | GET /api/review/queue | READ_ONLY_ANNOTATIONS | editor+ |
| 4 | get_assignments_for_user | review.ts (new) | GET /api/review/assignments | READ_ONLY_ANNOTATIONS | editor+ (non-admin auto-scoped to self) |
| 5 | create_review_assignment | review.ts (new) | POST /api/review/assignments | NON_IDEMPOTENT_WRITE_ANNOTATIONS | admin-only |
Registration order in tools/index.ts:
... governance (now 4 tools) → review (new, 3 tools) → intelligence (2) → guides (4)This groups review/governance tools together in tool-discovery order. Existing tool inventory numbers shift (intelligence moves from #42-43 to #47-48 etc.). Acceptable — inventory is regenerated on each ship.
3.2 get_governance_queue
Section titled “3.2 get_governance_queue”Wraps: GET /api/governance/review.
defineTool( server, 'get_governance_queue', { title: 'Get Governance Queue', description: 'List content items pending governance review. Returns each item with domain, due date, and reviewer. Use this to triage the governance backlog via Claude (weekly cadence for most admins).', inputSchema: { limit: z .number() .int() .min(1) .max(100) .default(20) .describe('Maximum items to return (default 20, max 100)'), offset: z .number() .int() .min(0) .default(0) .describe('Offset for pagination (default 0)'), domain: z .string() .optional() .describe( 'Optional domain filter (post-query filtering — the underlying route does not support domain filtering)', ), }, annotations: READ_ONLY_ANNOTATIONS, }, async (args, extra) => { /* ... */ },);Implementation outline. Use checkMcpRole(['admin', 'editor']). Query
content_items with governance_review_status = 'pending', same select list as
the route
(id, title, suggested_title, primary_domain, governance_review_status, governance_review_due, governance_reviewer_id, updated_by, updated_at),
ordered by governance_review_due asc nulls_last. Apply domain arg as a
post-query primary_domain filter (or inline into the query). Return markdown +
structuredContent.
Markdown shape:
## Governance Queue
**X items pending review** — showing Y-Z of W.
| # | Title | Domain | Due | Reviewer | Last Updated ||---|-------|--------|-----|----------|--------------|| 1 | "..." | compliance | 25/04/2026 | Rachel | 15/04/2026 |(If 0 items: “Governance queue is clear — no items pending review.”)
3.3 review_governance_item
Section titled “3.3 review_governance_item”Wraps: POST /api/governance/review.
defineTool( server, 'review_governance_item', { title: 'Process Governance Review Action', description: 'Process a governance review action on an item pending review. Actions: "approve" moves to approved, "request_changes" flags it back for editing, "revert" reverts the pending change. Does NOT handle publish/draft transitions — those live in update_governance_status. Editor or admin role required.', inputSchema: { item_id: z .string() .uuid() .describe( 'UUID of the content item to review (must currently have governance_review_status = "pending")', ), action: z .enum(['approve', 'request_changes', 'revert']) .describe('Review action to take'), notes: z .string() .max(1000) .optional() .describe( 'Optional reviewer notes (sent as part of the audit trail and notification)', ), }, annotations: NON_IDEMPOTENT_WRITE_ANNOTATIONS, }, async (args, extra) => { /* ... */ },);Implementation outline. Use checkMcpRole(['admin', 'editor']). Match the
route’s logic in app/api/governance/review/route.ts lines 80-213: fetch item,
verify pending status, switch on action to update governance_review_status +
governance_reviewer_id + governance_review_due, then best-effort
notification dispatch (use tryQuery for the item detail lookup, swallow
notification failures). Tool re-implements the route logic — it does NOT call
the route via HTTP (pattern mirrors all other MCP tools which talk to Supabase
directly).
Markdown shape:
## Governance review — approved
**Item:** Contract-management-policy ({item_id})**Action:** approve**Reviewer:** <user>**Notes:** "..." (if provided)**Status:** governance_review_status → approvedDifferentiator documented in description. The description explicitly says “Does NOT handle publish/draft transitions — those live in update_governance_status” so the LLM does not conflate the two.
3.4 get_review_queue
Section titled “3.4 get_review_queue”Wraps: GET /api/review/queue (non-flagged status path; flagged path
delegated to a follow-up if needed).
defineTool( server, 'get_review_queue', { title: 'Get Review Queue', description: 'List content items in the review queue. Filter by verification status, domain, content type. Used by the governance/review workflow to triage what needs reviewer attention.', inputSchema: { status: z .enum(['unverified', 'verified', 'flagged', 'draft', 'all']) .default('unverified') .describe('Verification-status filter'), limit: z.number().int().min(1).max(100).default(20), offset: z.number().int().min(0).default(0), domain: z.string().optional().describe('Optional primary_domain filter'), content_type: z .string() .optional() .describe('Optional content_type filter'), sort: z .enum(['created_at', 'confidence_asc', 'quality_score_asc']) .optional() .describe('Sort order (default created_at desc)'), }, annotations: READ_ONLY_ANNOTATIONS, }, async (args, extra) => { /* ... */ },);Implementation outline. Use checkMcpRole(['admin', 'editor']). Mirror the
non-flagged path in app/api/review/queue/route.ts lines 72-175 (content_items
select with REVIEW_COLUMNS, verification-status filter, optional domain +
content_type, sort, range pagination). Skip flagged status in v1 — if
status === 'flagged', return a friendly message: “Flagged items view not yet
available via MCP — use the web review queue for flagged items.” (Rationale:
flagged path joins ingestion_quality_log which adds complexity; can be added
in a follow-up if the skill needs it.)
Markdown shape: Similar table to governance queue, columns: #, Title,
Domain, Content type, Quality, Confidence, Verified?, Last reviewed.
Summary line with verified/flagged counts.
3.5 get_assignments_for_user
Section titled “3.5 get_assignments_for_user”Wraps: GET /api/review/assignments.
defineTool( server, 'get_assignments_for_user', { title: 'Get Review Assignments', description: 'List review assignments. Non-admin callers see only their own; admin callers see all assignments or filter by reviewer. Filter by status: active (default) / completed / cancelled / all.', inputSchema: { status: z .enum(['active', 'completed', 'cancelled', 'all']) .default('active'), reviewer_id: z .string() .uuid() .optional() .describe( 'Filter to a specific reviewer (admin-only — non-admins are always auto-scoped to themselves regardless of this value)', ), }, annotations: READ_ONLY_ANNOTATIONS, }, async (args, extra) => { /* ... */ },);Implementation outline. Use checkMcpRole(['admin', 'editor']) to gate the
tool at all. Inside: determine the user’s role via getMcpUserRole; if
non-admin, always filter reviewer_id = user.id regardless of the
reviewer_id arg (matches the API route’s line 55-57 behaviour). If admin and
reviewer_id supplied, filter to that reviewer; if admin and omitted, return
all.
Markdown shape: Table: #, Assignment ID (short), Reviewer,
Item count, Due, Status, Notes. Summary line: “N active / M completed
assignments for
3.6 create_review_assignment
Section titled “3.6 create_review_assignment”Wraps: POST /api/review/assignments (admin-only).
defineTool( server, 'create_review_assignment', { title: 'Create Review Assignment', description: 'Assign content items to a reviewer based on filter criteria. Computes the matching item count automatically. Notifies the assignee. Admin role required.', inputSchema: { reviewer_id: z .string() .uuid() .describe('UUID of the user to assign the review to'), filter_domains: z .array(z.string()) .default([]) .describe( 'Primary-domain filter (e.g. ["compliance", "audit-content"])', ), filter_content_types: z .array(z.string()) .default([]) .describe('content_type filter'), filter_freshness: z .array(z.string()) .default([]) .describe('freshness filter (fresh / aging / stale / expired)'), filter_date_from: z .string() .datetime() .nullable() .optional() .describe('ISO datetime — items captured on or after this date'), filter_date_to: z .string() .datetime() .nullable() .optional() .describe('ISO datetime — items captured on or before this date'), due_date: z .string() .datetime() .nullable() .optional() .describe('ISO datetime — assignment due date'), notes: z .string() .max(500) .nullable() .optional() .describe('Optional notes surfaced to the assignee'), }, annotations: NON_IDEMPOTENT_WRITE_ANNOTATIONS, }, async (args, extra) => { /* ... */ },);Implementation outline. Use checkMcpRole(['admin']) — strict admin gate.
Match route logic at app/api/review/assignments/route.ts lines 89-200: count
matching items via content_items head-count query with filters, insert
assignment row with assigned_by = user.id, best-effort notification dispatch
(swallow failures via try/catch — see the route for pattern).
Markdown shape:
## Review Assignment Created
**Assignment ID:** abc123...**Reviewer:** <reviewer_id>**Item count:** 12 items matching filter**Due:** 25/04/2026**Filter:**- Domains: compliance, audit-content- Content types: policy, case-study- Date range: 01/04/2026 to 21/04/2026**Notes:** "..."3.7 Registration wiring
Section titled “3.7 Registration wiring”File 1 — lib/mcp/tools/governance.ts: append 2 new tools after
update_governance_status. Update header comment from “(2 tools)” to “(4
tools)”. Import NON_IDEMPOTENT_WRITE_ANNOTATIONS + READ_ONLY_ANNOTATIONS.
File 2 — lib/mcp/tools/review.ts (NEW): registerReviewTools(server)
exporting 3 tools. Header comment matching existing patterns.
File 3 — lib/mcp/tools/index.ts: import + call registerReviewTools
between registerGovernanceTools and registerIntelligenceTools. Update header
comment: governance.ts (4) + add review.ts (3). Update total from “47 tools
across 12 category files” to “52 tools across 13 category files”.
File 4 — lib/mcp/formatters/review.ts (NEW): interfaces + format
functions: ReviewQueueItem, ReviewAssignment, CreateAssignmentResult +
formatReviewQueue(), formatAssignments(), formatCreateAssignment().
File 5 — lib/mcp/formatters/governance.ts (existing): add
GovernanceQueueItem + GovernanceReviewActionResult interfaces +
formatGovernanceQueue() + formatGovernanceReviewAction() formatters.
File 6 — lib/mcp/formatters/index.ts: re-export new formatters.
3.8 Fixtures + inventory
Section titled “3.8 Fixtures + inventory”scripts/mcp-eval/fixtures.ts— add 5 tool names toCANONICAL_TOOL_NAMESin registration order (governance additions afterupdate_governance_status; review tools grouped after governance). UpdateTOOL_COUNTcomment.- Update ai-tools flag set if applicable (none of these call Claude APIs — no
change to
AI_TOOLSneeded). docs/generated/mcp-inventory.md+.json— regen viabun run generate:mcp-inventory.lib/mcp/plugin-bundle.ts— regen viabun run build:plugin(plugin docs reference tool names — no breaking change but bundle content changes).
4. Acceptance criteria
Section titled “4. Acceptance criteria”| AC | Criterion |
|---|---|
| AC-1 | 5 tools registered via defineTool with the names, annotations, and role gates in §3.1. Total tool count 47 → 52. |
| AC-2 | get_governance_queue paginates via limit + offset, supports optional domain post-filter, orders by governance_review_due asc. Returns same column set as the API route. |
| AC-3 | review_governance_item implements the 3 actions (approve / request_changes / revert) with the same DB effects as POST /api/governance/review. Notification dispatch is best-effort. Description explicitly distinguishes from update_governance_status. |
| AC-4 | get_review_queue supports status (unverified / verified / flagged / draft / all) + domain + content_type + sort. flagged status returns a friendly “not yet available via MCP” message. Mirrors non-flagged route logic. |
| AC-5 | get_assignments_for_user auto-scopes non-admin callers to their own reviewer_id regardless of the reviewer_id arg (mirrors API line 55-57). Admin can query any reviewer or all. |
| AC-6 | create_review_assignment is admin-only, computes item_count via filter head-count query, inserts with assigned_by = user.id, best-effort notification. Matches route body schema. |
| AC-7 | All 5 tools return markdown + toStructuredContent(...) with well-structured JSON. isError: true for permission denials. |
| AC-8 | C-1 audit: no rename, no removal of any existing tool / prompt / resource. update_governance_status stays unchanged. |
| AC-9 | New test files exercise: (a) admin-only gate rejection for create_review_assignment; (b) editor+ gate for the other 4; (c) happy-path for each; (d) empty-list behaviour; (e) non-admin reviewer_id override does not escape self-scope; (f) get_review_queue flagged status friendly message. At least one integration test per tool using createMockSupabaseClient. |
| AC-10 | scripts/mcp-eval/fixtures.ts CANONICAL_TOOL_NAMES includes the 5 new names. mcp-fixture-sync.test.ts passes against the new count. |
| AC-11 | tool-annotations-coverage.test.ts passes — the 5 new tools have all 4 annotation fields explicit via defineTool. |
| AC-12 | docs/generated/mcp-inventory.md regenerated; summary line shows “52 tools”. |
| AC-13 | bun run test green. bun lint green. bun run build:plugin clean. |
5. Implementation plan
Section titled “5. Implementation plan”File-level plan
Section titled “File-level plan”| # | File | Action | Est LOC |
|---|---|---|---|
| 1 | lib/mcp/tools/governance.ts | Extend: +2 tools (get_governance_queue, review_governance_item) | +180 |
| 2 | lib/mcp/tools/review.ts | NEW: registerReviewTools with 3 tools | +300 |
| 3 | lib/mcp/tools/index.ts | Register registerReviewTools + update comments | +3 |
| 4 | lib/mcp/formatters/review.ts | NEW: interfaces + 3 formatters | +150 |
| 5 | lib/mcp/formatters/governance.ts | Extend: +2 formatters | +60 |
| 6 | lib/mcp/formatters/index.ts | Re-export new formatters | +4 |
| 7 | scripts/mcp-eval/fixtures.ts | Add 5 tool names + update count | +7 |
| 8 | __tests__/mcp/review-tools.test.ts | NEW: 3 review tools | +300 |
| 9 | __tests__/mcp/governance-queue-tools.test.ts | NEW: 2 governance additions | +200 |
| 10 | docs/generated/mcp-inventory.md + .json | Regen | — |
| 11 | lib/mcp/plugin-bundle.ts | Regen | — |
Net ~1200 LOC across 9 hand-edited files.
Sequencing
Section titled “Sequencing”- Spec (done).
- Formatter scaffolding —
formatters/review.ts+formatters/governance.tsextensions +formatters/index.tsre-exports. - Tool impl —
tools/review.tsnew file,tools/governance.tsextension. - Registration + inventory —
tools/index.ts+ fixtures. - Tests — mock Supabase client pattern from existing
__tests__/mcp/*-tool*.test.ts. - Regen bundle + inventory.
- Full
bun run test+bun lint. - Single commit with all files.
6. Tests
Section titled “6. Tests”Review tools (__tests__/mcp/review-tools.test.ts)
Section titled “Review tools (__tests__/mcp/review-tools.test.ts)”Pattern mirrors __tests__/mcp/intelligence-tools.test.ts. Covers:
get_review_queue: editor+ gate; happy-path with mock supabase returning 2 rows;status='flagged'friendly-message path; domain filter applied; pagination offset respected.get_assignments_for_user: editor+ gate; non-admin caller withreviewer_idarg pointing to someone else still receives only own assignments (self-scope enforcement); admin can query any reviewer; status filter applied.create_review_assignment: admin-only gate (editor rejection); item-count computation; insert call args; notification fires (mock verified); notification failure does not fail the tool.
Governance tools (__tests__/mcp/governance-queue-tools.test.ts)
Section titled “Governance tools (__tests__/mcp/governance-queue-tools.test.ts)”get_governance_queue: editor+ gate; empty-state; happy-path with 3 pending rows; domain post-filter.review_governance_item: editor+ gate; pending-status precondition (409-style friendly error if not pending); each of 3 actions produces the correctgovernance_review_statusupdate; notification dispatch is best-effort.
Sync guards
Section titled “Sync guards”mcp-fixture-sync.test.tsalready drives offCANONICAL_TOOL_NAMES; updating the fixture auto-covers.tool-annotations-coverage.test.ts— verifies every tool has all 4 annotation fields.defineToolenforces at compile time, but this runtime-asserts.mcp-inventory-parser.test.ts— add 5 toexpect(tools.length).toBe(47)assertion → 52.
7. Breaking change audit (C-1)
Section titled “7. Breaking change audit (C-1)”| Surface | Check | Result |
|---|---|---|
| Existing tools (47) | Any renamed? | No — all 47 retain their names and signatures. |
| Existing tools | Any semantic change to update_governance_status? | No — behaviour unchanged. This tool continues to handle publish/draft; the new review_governance_item is explicitly distinguished. |
| MCP prompts (7) | Any changed? | No (§WP1 + §WP2 already shipped; WP3 adds only tools). |
| MCP resources (12) | Any changed? | No. |
| Tool category registration order | Shifts intelligence + guides tool numbers by 5 | Cosmetic — inventory is regenerated. MCP clients treat tools by name, not position. |
| Plugin commands | Any changed? | No. |
| Plugin bundle | Bytes change due to tool-list regen | Expected. Plugin clients fetch on each session. |
| Public APIs / DB schema | Any migration / API change? | No — tools are thin wrappers over unchanged routes + unchanged tables. |
Verdict: additive only. C-1 audit PASS.
8. Open questions
Section titled “8. Open questions”None blocking. Noted risk: registration order renumbers existing tools in inventory — cosmetic but will show up in diffs. Preferred over appending review tools at the end (which would split the governance group awkwardly).
9. References
Section titled “9. References”- Subagent investigation summary: embedded in S180 session conversation (Liam confirmation of A1 + B2 scope).
- DECISIONS §3.1 P0-23 row
- SPEC-SEQUENCE line 265 (2h spec budget — note SPEC-SEQUENCE only named 2 net-new tools; this spec ships 5 per Liam confirmation)
- Existing:
lib/mcp/tools/governance.ts,app/api/review/queue/route.ts,app/api/review/assignments/route.ts,app/api/governance/review/route.ts,lib/validation/schemas.ts - Pattern reference:
lib/mcp/tools/intelligence.ts(admin-only write tool with lazy imports);lib/mcp/tools/guides.ts(multi-tool category file).