TSG-05: MCP Process Lifecycle & Agent WebSocket Recovery¶
1. Symptoms & Alerts¶
- Launching
uns-mcp --enable-writesexits immediately with a parser error (Set UNS_DATABASE_URL...,Write-enabled MCP requires a persistent database,Set UNS_API_PRINCIPAL..., orSet UNS_MCP_DELIVERY_CONFIRMATION_TOKEN...). - Calling MCP
send_signalwithdeliver=true,ack_notification, ordismiss_notificationfails withValueError. - An MCP process terminates while a retry or callback is pending, and operators need to verify recovery.
- Agent WebSocket connections to
/ws/agents/{agent_id}fail during handshake with WebSocket close code1008(WS_1008_POLICY_VIOLATION).
2. Root Cause & Component Mechanics¶
2.1 uns-mcp Security & Lifecycle Model (src/uns_mcp/)¶
- Read-Only by Default: Running
uns-mcpwithout--enable-writesstarts inmode="stdio-local"withread_only=True. In this mode,simulate_signaland read tools (list_deliveries,list_dlq,get_notification_status, etc.) work, whilesend_signal(deliver=True),ack_notification, anddismiss_notificationare blocked. - Runtime-Bound Read Helpers (
F-07): - Both the FastMCP server tools and direct Python helpers in
uns_mcp.toolsacceptruntime_state=RuntimeState(runtime=runtime)and read directly from that runtime's durable SQLite or PostgreSQL outbox and audit stores. - Write-Enabled Requirements (
uns-mcp --enable-writes): UNS_DATABASE_URLmust point to a persistent SQLite file (sqlite:////path/to/uns.db) or PostgreSQL database (postgresql+psycopg://...); volatile in-memory URLs (sqlite://and:memory:) are rejected.UNS_API_PRINCIPALmust be set to a non-empty actor identifier.UNS_MCP_DELIVERY_CONFIRMATION_TOKENmust be set to a non-empty secret.- Every
send_signalcall withdeliver=truemust also supplyoperator_intent="deliver"andoperator_confirmation_tokenmatchingUNS_MCP_DELIVERY_CONFIRMATION_TOKEN(verified via constant-timehmac.compare_digest). - In-Process Recovery Worker (
uns-mcp-worker): - When
uns-mcp --enable-writesstarts its FastMCP lifespan (src/uns_mcp/factory.py), it spawnsasyncio.create_task(runtime.worker(), name="uns-mcp-worker"). - On
SIGINTorSIGTERM,_serve()cancels the stdio task and usesanyio.CancelScope(shield=True)during lifespan teardown to cancel and join the recovery worker cleanly before disposing the SQLAlchemy engine.
2.2 Agent WebSocket Authentication (/ws/agents/{agent_id})¶
Defined in src/uns/api/auth.py:
- Query-string tokens (?api_key=... or ?token=...) are never accepted.
- If an Origin header is present, it must match UNS_ALLOWED_ORIGINS (default http://localhost:5173,http://127.0.0.1:5173).
- Native clients pass Authorization: Bearer <UNS_API_KEY> (or X-UNS-API-Key).
- Browser clients pass Sec-WebSocket-Protocol: uns.v1, bearer.<base64url_nopad(UNS_API_KEY)>.
3. Diagnostic Commands¶
Step 3.1: Verify MCP environment variables before starting uns-mcp --enable-writes¶
python3 -c '
from uns_mcp.transports.stdio import runtime_config
cfg = runtime_config(enable_writes=True)
print("MCP Write Config Valid:", cfg.mode, "principal=", cfg.operator_principal)
'
Step 3.2: Check for orphaned or pending work after an MCP process exit¶
sqlite3 "$UNS_DB_FILE" "
SELECT id, principal, status, attempts, cycle, lease_until, next_attempt
FROM uns_notifications
WHERE status = 'PENDING'
ORDER BY next_attempt ASC;
"
4. Safe Remediation Steps¶
Scenario A: send_signal with deliver=true raises ValueError¶
Check the exact error string:
1. send_signal is disabled in read-only mode: Launch uns-mcp with --enable-writes and the three required environment variables (UNS_DATABASE_URL, UNS_API_PRINCIPAL, UNS_MCP_DELIVERY_CONFIRMATION_TOKEN).
2. send_signal deliver=true requires operator_intent='deliver': Include "operator_intent": "deliver" in the tool arguments.
3. send_signal delivery confirmation token is invalid: Ensure "operator_confirmation_token" in the tool payload matches the host's UNS_MCP_DELIVERY_CONFIRMATION_TOKEN environment variable.
Scenario B: ack_notification or dismiss_notification raises ValueError¶
ack_notification acked_by must match the operator principal: Either omitacked_byfromAckNotificationInputor set it to the exact value ofUNS_API_PRINCIPAL.
Scenario C: WebSocket connection fails with 1008 Policy Violation¶
- Reason
"Origin is not allowed": Add your frontend/agent origin toUNS_ALLOWED_ORIGINS(comma-separated, e.g.,export UNS_ALLOWED_ORIGINS="https://console.example.com"). - Reason
"Authentication required": - For Python/Node/Go clients: pass
Authorization: Bearer <UNS_API_KEY>in the handshake headers. - For Browser clients: pass
["uns.v1", "bearer.<base64url-unpadded-key>"]as theWebSocketsubprotocols argument.
5. Operational Best Practices¶
Important
Short-Lived Stdio MCP Sessions & Recovery Continuity:
uns-mcp --enable-writes runs runtime.worker() only while the uns-mcp stdio process is alive.
- If
send_signal(deliver=True)experiences a transient adapter failure on Attempt 1 (leavingstatus = "PENDING"withnext_attempt = now + 1.0) and the MCP host immediately terminates theuns-mcpstdio process, the stoppeduns-mcpprocess can no longer run Attempt 2. - Operational Mitigation: In production, always run the long-lived
unsservice (or a dedicated persistent worker process) attached to the sameUNS_DATABASE_URLso that background retries, lease recovery, and timeout sweeps continue even when stdio MCP client sessions disconnect.