Safety Patterns in StockTrim MCP Server¶
This document describes the safety patterns and user confirmation mechanisms implemented in the StockTrim MCP Server to prevent accidental data loss and ensure safe operation of high-risk tools.
Overview¶
The StockTrim MCP Server implements a Human-in-the-Loop (HITL) pattern for operations that could result in permanent data loss or significant business impact. This pattern uses the MCP protocol's native elicitation feature to request explicit user confirmation before executing destructive operations.
Risk Classification¶
All tools in the MCP server are classified by risk level to determine whether user confirmation is required:
🔴 HIGH-RISK (Requires Confirmation)¶
Operations that permanently delete data and cannot be undone:
delete_product- Permanently deletes a product from inventorydelete_supplier- Permanently deletes a supplier and all associations (product mappings, purchase order history)delete_purchase_order- Permanently deletes a purchase orderdelete_sales_orders- Permanently deletes all sales orders for a product (bulk deletion)
Pattern: All HIGH-RISK operations require user confirmation via MCP elicitation with detailed preview before execution.
🟡 MEDIUM-HIGH RISK (May Require Confirmation)¶
Operations that modify critical business data in ways that could cause operational issues:
set_product_inventory- Directly sets inventory levels (bypassing normal stock movements)configure_product- Changes product lifecycle/forecasting configurationproducts_configure_lifecycle- Bulk product lifecycle configuration changesupdate_forecast_settings- Changes global forecasting parameters
Pattern: May be implemented in Phase 2 with confirmation for bulk operations or critical threshold changes.
🟠 MEDIUM RISK (Contextual Confirmation)¶
Operations that create financial obligations or commitments:
create_purchase_order- Creates financial commitment to suppliercreate_sales_order- Creates customer order and inventory commitmentgenerate_purchase_orders_from_urgent_items- Automated bulk PO generation
Pattern: May be implemented in Phase 3 with confirmation for high-value or bulk operations.
🟢 LOW RISK (No Confirmation)¶
Reversible create operations:
create_product,create_supplier,create_customer,create_location
Pattern: No confirmation required - these can be reversed by deleting the created entity.
⚪ SAFE (No Confirmation)¶
Read-only operations:
- All
get_*,list_*,search_*,find_*tools
Pattern: No confirmation required - read operations have no side effects.
Elicitation Pattern¶
The MCP server uses FastMCP's elicitation API, which implements the MCP specification's native confirmation protocol.
Basic Flow¶
from fastmcp.server.elicitation import (
AcceptedElicitation,
CancelledElicitation,
DeclinedElicitation,
)
async def high_risk_tool(request: Request, context: Context) -> Response:
"""High-risk tool with user confirmation."""
# 1. Fetch entity details
entity = await get_entity(request.id)
if not entity:
return Response(success=False, message="Entity not found")
# 2. Build preview information
preview = build_preview(entity)
# 3. Request user confirmation
result = await context.elicit(
message=f"""⚠️ Delete {entity.name}?
{preview}
This action will permanently delete the data and cannot be undone.
Proceed with deletion?""",
response_type=None, # Simple yes/no approval
)
# 4. Handle elicitation response
match result:
case AcceptedElicitation():
# User confirmed - proceed
success, message = await delete_entity(entity.id)
return Response(
success=success,
message=f"✅ {message}" if success else message,
)
case DeclinedElicitation():
# User declined
return Response(
success=False,
message=f"❌ Deletion declined by user",
)
case CancelledElicitation():
# User cancelled
return Response(
success=False,
message=f"❌ Deletion cancelled by user",
)
case _:
# Unexpected response
return Response(
success=False,
message="Unexpected elicitation response",
)
Preview Guidelines¶
The preview shown to the user should include:
- Entity Identification: Code, name, or unique identifier
- Critical Context: Status, relationships, financial impact
- Impact Warning: Clear statement of what will be deleted/affected
- Irreversibility Notice: Explicit statement that action cannot be undone
Example Previews¶
Product Deletion:
⚠️ Delete product WIDGET-001?
🟢 **Blue Widget**
Status: Active
This action will permanently delete the product and cannot be undone.
Proceed with deletion?
Purchase Order Deletion:
⚠️ Delete purchase order PO-2024-001?
**Supplier**: Acme Supplies (SUP-001)
**Status**: Draft
**Total Cost**: $1,550.00
**Line Items**: 3 items
This action will permanently delete the purchase order and cannot be undone.
Proceed with deletion?
Bulk Sales Order Deletion:
⚠️ Delete 15 sales orders for product WIDGET-001?
**Orders to Delete**: 15
**Total Quantity**: 450.0
**Customers Affected**: 8 customers
**Total Revenue**: $13,455.00
This action will permanently delete all sales orders for this product and cannot be undone.
Proceed with deletion?
Response Handling¶
Always handle all three elicitation response types:
- AcceptedElicitation: User confirmed - proceed with operation
- DeclinedElicitation: User explicitly declined - abort operation
- CancelledElicitation: User cancelled prompt - abort operation
For success responses, prefix with ✅ emoji. For declined/cancelled responses, prefix with ❌ emoji.
Testing Elicitation¶
Elicitation behavior should be thoroughly tested:
@pytest.mark.asyncio
async def test_delete_with_confirmation_accepted(mock_context, sample_entity):
"""Test deletion when user accepts confirmation."""
# Setup
services = mock_context.request_context.lifespan_context
services.entity.get.return_value = sample_entity
services.entity.delete.return_value = (True, "Entity deleted")
mock_context.elicit = AsyncMock(return_value=AcceptedElicitation(data=None))
# Execute
request = DeleteRequest(id="test-id")
response = await delete_entity(request, mock_context)
# Verify response
assert response.success is True
assert "✅" in response.message
# Verify elicitation was called with preview
mock_context.elicit.assert_called_once()
elicit_args = mock_context.elicit.call_args
assert "⚠️ Delete" in elicit_args[1]["message"]
assert sample_entity.name in elicit_args[1]["message"]
# Verify deletion was called
services.entity.delete.assert_called_once_with("test-id")
@pytest.mark.asyncio
async def test_delete_with_confirmation_declined(mock_context, sample_entity):
"""Test deletion when user declines confirmation."""
# Setup
services = mock_context.request_context.lifespan_context
services.entity.get.return_value = sample_entity
mock_context.elicit = AsyncMock(return_value=DeclinedElicitation(data=None))
# Execute
request = DeleteRequest(id="test-id")
response = await delete_entity(request, mock_context)
# Verify response
assert response.success is False
assert "❌" in response.message
assert "declined" in response.message
# Verify deletion was NOT called
services.entity.delete.assert_not_called()
Required test cases for each elicitation tool:
- Not Found: Entity doesn't exist - no elicitation, early return
- Accepted: User accepts - elicitation called, deletion proceeds
- Declined: User declines - elicitation called, deletion aborted
- Cancelled: User cancels - elicitation called, deletion aborted
- Preview Content: Verify preview includes expected entity details
Implementation Status¶
Phase 1: HIGH-RISK Deletions ✅¶
All HIGH-RISK deletion operations now require user confirmation:
- ✅
delete_product- products.py:214-296 - ✅
delete_supplier- suppliers.py:204-289 - ✅
delete_purchase_order- purchase_orders.py:370-464 - ✅
delete_sales_orders- sales_orders.py:290-404
Test coverage: 20 tests added (5 per tool)
Future Phases¶
Phase 2: MEDIUM-HIGH RISK modifications - Planned
- Confirmation for bulk inventory changes
- Confirmation for critical configuration changes
Phase 3: MEDIUM RISK creates - Planned
- Confirmation for high-value purchase orders
- Confirmation for bulk automated operations
Best Practices¶
For Tool Developers¶
- Always fetch entity first: Get full entity details before elicitation to build accurate preview
- Build rich previews: Include all relevant context (financial impact, relationships, status)
- Handle all response types: Always handle Accepted, Declined, Cancelled, and unexpected responses
- Test thoroughly: Write tests for all elicitation paths (not found, accepted, declined, cancelled, preview content)
- Document risk level: Add 🔴 HIGH-RISK OPERATION notice to docstrings
For MCP Clients¶
- Present elicitation UI: Show elicitation message prominently with clear accept/decline options
- Preserve formatting: Maintain markdown formatting and emoji in preview
- Allow cancellation: Always provide a way for users to cancel/dismiss the prompt
- Log responses: Record whether user accepted/declined for audit purposes
Security Considerations¶
Why Elicitation?¶
The MCP elicitation pattern provides stronger safety guarantees than alternatives:
- Hard Stop: Operation cannot proceed without explicit user approval
- Protocol-Level: Built into MCP specification, not application-level workaround
- Client-Side UI: Confirmation happens in user-facing client, not hidden in logs
- Audit Trail: Clients can log elicitation responses for compliance
Defense in Depth¶
Elicitation is the first line of defense. Additional safeguards:
- API-Level Validation: StockTrim API validates all operations
- Service Layer Checks: Service methods validate business rules before API calls
- Database Constraints: Foreign keys and constraints prevent invalid deletions
- Soft Deletes: Consider implementing soft deletes in StockTrim API (future enhancement)
References¶
- MCP Specification - Elicitation
- FastMCP Documentation - Elicitation
- ADR 001: User Confirmation Pattern
- Issue #80: Add user confirmation for destructive operations
Support¶
For questions or issues with safety patterns: