Skip to main content

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}