Task — engineering-spec@1

"MCP destructive-tool gating via elicitation confirmation"

doneTASK-MCP-006
module mcp · class product · priority p0 · created 2026-05-17 · shipped null
depends on TASK-MCP-001, TASK-MCP-004 · blocks none

TASK-MCP-006: MCP destructive-tool gating via elicitation confirmation

Summary

Ship gateway-entry gating so a destructiveHint=true tools/call is held for a TASK-MCP-008 confirmation elicitation before the gateway forwards to the module. As-built surface is a single gating.rs decision module plus ToolAnnotations in annotations.rs, wired from router.rs (in-memory store or elicitation_pg when a DB + authenticated caller are present). Non-destructive tools fast-path through; decline returns an in-band user_rejected tool error.

Problem

MCP 2025-11-25 advertises tool annotations, but without a gateway gate a destructive tool still executes on the first tools/call. Client UX prompts are optional; the server must refuse to forward until the caller confirms. The original engineering-spec imagined a 7-file gating/ tree, per-tenant YAML policy, bypass tokens, confirm-TTL tables, and ten gating_* integration test files under services/mcp/ — none of that matches the shipped services/mcp-gateway/ crate.

Proposed Solution

Keep the decision pure in services/mcp-gateway/src/gating.rs:

router.rs consults entry.annotations.destructive_hint on tools/call. On hold it creates a confirmation elicitation (ElicitationStore or elicitation_pg::create_confirmation), returns the held result, and on re-invoke with _meta.confirmation_id re-evaluates and either forwards or aborts. annotations.rs exposes ToolAnnotations (including read_only_idempotent / destructive constructors) for tools/list and the gate.

Alternatives Considered

Success Metrics

Scope

In scope (as-built under services/mcp-gateway/):

Out of scope / Non-Goals

Dependencies

depends_on: [TASK-MCP-001, TASK-MCP-004] — protocol tools/call path and OAuth-authenticated callers for the DB store-of-record path. Soft: TASK-MCP-008 supplies confirmation elicitation (in-memory + elicitation_pg + migration 0016). Related: TASK-MCP-007 (future long-running destructive confirm-at-start is not required for this adopt).

1. Description (normative)

Acceptance criteria

Verification

Run from repo root / services/:

cd services && cargo test -p cyberos-mcp-gateway gating
cd services && cargo test -p cyberos-mcp-gateway annotations
cd services && cargo test -p cyberos-mcp-gateway --lib router::tests::destructive_
cd services && cargo test -p cyberos-mcp-gateway --lib router::tests::read_only_tool_forwards_through_the_gate

Real in-crate tests (do not cite non-existent gating_* integration files):

PathCovers
services/mcp-gateway/src/gating.rs (non_destructive_always_proceeds, destructive_holds_then_proceeds_or_declines, held_result_carries_the_elicitation_and_is_not_an_error, user_rejected_result_is_an_in_band_error)Pure gate decision + result shapes
services/mcp-gateway/src/annotations.rs (read_only_idempotent_flips_correct_hints, destructive_flips_correct_hints)Hint constructors
services/mcp-gateway/src/router.rs (read_only_tool_forwards_through_the_gate, destructive_tool_without_confirmation_is_held, destructive_tool_with_confirmation_forwards, destructive_tool_declined_aborts_cleanly)End-to-end tools/call gating with in-memory elicitation

Persistence of confirmations is verified under TASK-MCP-008 (elicitation_pg + db_slice_test.rs::elicitation_persists_seals_and_is_caller_scoped, Postgres-gated / #[ignore] until a pool is available).

AI Authorship Disclosure


batch/9a-mcp adopt — TASK-MCP-006 re-spec against as-built mcp-gateway gating.