mcpr_core/proxy/state.rs
1//! Proxy runtime state — shared across the proxy process.
2//!
3//! Tracks the health and status of all proxy connections: MCP upstream,
4//! tunnel, widgets, cloud sync, and request counters. This is the single
5//! source of truth for "what is the proxy doing right now?"
6//!
7//! Used by:
8//! - `mcpr start` — populates state during operation.
9//! - `mcpr status` — could read state via admin API (future).
10//! - `mcpr proxy view` — TUI viewer will read this state (future).
11//! - Health check loop — updates MCP/widget/tunnel status periodically.
12
13use std::sync::{Arc, Mutex};
14use std::time::Instant;
15
16/// Connection status for an upstream service (MCP, tunnel, widgets).
17#[derive(Clone, Copy, PartialEq)]
18pub enum ConnectionStatus {
19 /// Status not yet determined (initial state).
20 Unknown,
21 /// Service is unreachable or down.
22 Disconnected,
23 /// Connection attempt in progress (tunnel only).
24 Connecting,
25 /// Service is healthy and responding.
26 Connected,
27 /// Tunnel was forcibly disconnected by the relay (e.g., subdomain conflict).
28 Evicted,
29 /// Server is reachable but does not speak MCP protocol.
30 NotMcp,
31}
32
33impl ConnectionStatus {
34 pub fn label(&self) -> &'static str {
35 match self {
36 Self::Unknown => "Unknown",
37 Self::Disconnected => "Disconnected",
38 Self::Connecting => "Connecting…",
39 Self::Connected => "Connected",
40 Self::Evicted => "Evicted",
41 Self::NotMcp => "Not MCP",
42 }
43 }
44}
45
46/// Cloud sync status from the last flush attempt.
47pub enum CloudSyncStatus {
48 Ok { count: usize },
49 Failed { message: String },
50}
51
52/// Runtime state of a running proxy instance.
53///
54/// All fields are updated by background tasks (health checks, request handlers)
55/// and read by status/admin endpoints. Protected by a Mutex for simplicity —
56/// contention is negligible since updates are infrequent (every few seconds).
57pub struct ProxyState {
58 /// Public URL where AI clients connect (e.g., http://localhost:3000 or tunnel URL).
59 pub proxy_url: String,
60 /// Tunnel public URL (empty if tunnel disabled).
61 pub tunnel_url: String,
62 /// Upstream MCP server URL from config.
63 pub mcp_upstream: String,
64 /// Widget source description ("URL", "path", or "(none)").
65 pub widgets: String,
66
67 /// MCP upstream connection health.
68 pub mcp_status: ConnectionStatus,
69 /// Optional warning about MCP upstream (e.g., "Server requires auth").
70 pub mcp_warning: Option<String>,
71 /// Tunnel connection health.
72 pub tunnel_status: ConnectionStatus,
73 /// Widget source connection health.
74 pub widgets_status: ConnectionStatus,
75 /// Number of discovered widgets.
76 pub widget_count: Option<usize>,
77 /// Names of discovered widgets.
78 pub widget_names: Vec<String>,
79
80 /// Cloud sync endpoint URL (None if cloud not configured).
81 pub cloud_endpoint: Option<String>,
82 /// Last cloud sync status.
83 pub cloud_sync: Option<CloudSyncStatus>,
84
85 /// When this proxy instance started.
86 pub started_at: Instant,
87 /// Total number of requests handled.
88 pub request_count: u64,
89}
90
91impl ProxyState {
92 pub fn new() -> Self {
93 Self {
94 proxy_url: String::new(),
95 tunnel_url: String::new(),
96 mcp_upstream: String::new(),
97 widgets: "(none)".into(),
98 mcp_status: ConnectionStatus::Unknown,
99 mcp_warning: None,
100 tunnel_status: ConnectionStatus::Disconnected,
101 widgets_status: ConnectionStatus::Unknown,
102 widget_count: None,
103 widget_names: Vec::new(),
104 cloud_endpoint: None,
105 cloud_sync: None,
106 started_at: Instant::now(),
107 request_count: 0,
108 }
109 }
110
111 /// Mark the MCP upstream as confirmed connected (clear any warning).
112 pub fn confirm_mcp_connected(&mut self) {
113 self.mcp_status = ConnectionStatus::Connected;
114 self.mcp_warning = None;
115 }
116
117 /// Increment request counter.
118 pub fn record_request(&mut self) {
119 self.request_count += 1;
120 }
121
122 /// Human-readable uptime string.
123 pub fn uptime(&self) -> String {
124 let secs = self.started_at.elapsed().as_secs();
125 if secs < 60 {
126 format!("{secs}s")
127 } else if secs < 3600 {
128 format!("{}m {}s", secs / 60, secs % 60)
129 } else {
130 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
131 }
132 }
133}
134
135impl Default for ProxyState {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141/// Thread-safe shared proxy state.
142///
143/// Use [`lock_state`] instead of `.lock().unwrap()` to handle mutex
144/// poisoning gracefully (recovers instead of panicking).
145pub type SharedProxyState = Arc<Mutex<ProxyState>>;
146
147/// Create a new shared proxy state.
148pub fn new_shared_state() -> SharedProxyState {
149 Arc::new(Mutex::new(ProxyState::new()))
150}
151
152/// Lock the shared state, recovering from poison if a thread panicked.
153///
154/// Prefer this over `.lock().unwrap()` — it never panics.
155pub fn lock_state(state: &SharedProxyState) -> std::sync::MutexGuard<'_, ProxyState> {
156 state.lock().unwrap_or_else(|e| e.into_inner())
157}