|
| 1 | +# Unified Health Status Design |
| 2 | + |
| 3 | +**Date**: 2025-12-10 |
| 4 | +**Status**: Ready for implementation |
| 5 | + |
| 6 | +## Problem Statement |
| 7 | + |
| 8 | +**Current issues:** |
| 9 | +1. **Inconsistent status** - CLI, tray, and web show different health interpretations |
| 10 | +2. **Missing OAuth visibility** - Token expiration not shown in tray/web |
| 11 | +3. **No actionable guidance** - Users see errors but not how to fix them |
| 12 | +4. **Conflated concepts** - Admin state (disabled/quarantined) mixed with health |
| 13 | + |
| 14 | +**Root cause:** Each interface calculates status independently from raw fields, leading to drift. For example: |
| 15 | +- CLI reads `oauth_status` and shows "Token Expired" |
| 16 | +- Tray only checks HTTP connectivity and shows "Healthy" |
| 17 | +- Same server, different conclusions |
| 18 | + |
| 19 | +**Goals:** |
| 20 | +- Single source of truth for server health in the backend |
| 21 | +- Consistent display across CLI, tray, and web UI |
| 22 | +- Traffic light model: healthy (green) / degraded (yellow) / unhealthy (red) |
| 23 | +- Every degraded/unhealthy state includes an action to resolve it |
| 24 | +- Admin state (enabled/disabled/quarantined) shown separately from health |
| 25 | + |
| 26 | +**Non-goals:** |
| 27 | +- Changing OAuth flow mechanics |
| 28 | +- Adding new OAuth features |
| 29 | +- Redesigning the web UI layout |
| 30 | + |
| 31 | +## Data Model |
| 32 | + |
| 33 | +**New `HealthStatus` struct** (in `internal/contracts/types.go`): |
| 34 | + |
| 35 | +```go |
| 36 | +type HealthStatus struct { |
| 37 | + Level string `json:"level"` // "healthy", "degraded", "unhealthy" |
| 38 | + AdminState string `json:"admin_state"` // "enabled", "disabled", "quarantined" |
| 39 | + Summary string `json:"summary"` // "Connected (5 tools)", "Token expiring in 2h" |
| 40 | + Detail string `json:"detail"` // Optional longer explanation |
| 41 | + Action string `json:"action"` // "login", "restart", "enable", "approve", "view_logs", "" |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +**Added to existing `Server` struct:** |
| 46 | + |
| 47 | +```go |
| 48 | +type Server struct { |
| 49 | + // ... existing fields ... |
| 50 | + Health HealthStatus `json:"health"` // New unified health status |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +**Level values:** |
| 55 | +| Level | Meaning | View convention | |
| 56 | +|-------|---------|-----------------| |
| 57 | +| `healthy` | Ready to use, no issues | green | |
| 58 | +| `degraded` | Works but needs attention soon | yellow | |
| 59 | +| `unhealthy` | Broken, can't use until fixed | red | |
| 60 | + |
| 61 | +**Action types:** |
| 62 | +| Action | Meaning | |
| 63 | +|--------|---------| |
| 64 | +| `""` | No action needed (healthy state) | |
| 65 | +| `login` | OAuth authentication required | |
| 66 | +| `restart` | Server needs restart | |
| 67 | +| `enable` | Server is disabled | |
| 68 | +| `approve` | Server is quarantined | |
| 69 | +| `view_logs` | Check logs for details | |
| 70 | + |
| 71 | +## Health Calculation Logic |
| 72 | + |
| 73 | +**Location:** `internal/runtime/runtime.go` in `GetAllServers()` (or extracted to `internal/health/calculator.go`) |
| 74 | + |
| 75 | +**Priority order** (first match wins): |
| 76 | + |
| 77 | +``` |
| 78 | +1. Admin state checks (shown instead of health when not enabled) |
| 79 | + - quarantined → AdminState: "quarantined" |
| 80 | + - disabled → AdminState: "disabled" |
| 81 | +
|
| 82 | +2. Unhealthy (red) conditions |
| 83 | + - connection refused/failed → "unhealthy", Action: "restart" |
| 84 | + - auth failed (bad credentials) → "unhealthy", Action: "login" |
| 85 | + - server crashed → "unhealthy", Action: "restart" |
| 86 | + - config error → "unhealthy", Action: "view_logs" |
| 87 | + - token expired → "unhealthy", Action: "login" |
| 88 | + - refresh failed (after retries)→ "unhealthy", Action: "login" |
| 89 | + - user logged out → "unhealthy", Action: "login" |
| 90 | +
|
| 91 | +3. Degraded (yellow) conditions |
| 92 | + - token expiring soon, no refresh token → "degraded", Action: "login" |
| 93 | + - connecting (in progress) → "degraded", Action: "" |
| 94 | +
|
| 95 | +4. Healthy (green) |
| 96 | + - connected + authenticated (OAuth servers) |
| 97 | + - connected (non-OAuth servers) |
| 98 | + - token valid OR auto-refresh working |
| 99 | +``` |
| 100 | + |
| 101 | +**OAuth-specific logic:** |
| 102 | +| Condition | Level | Action | |
| 103 | +|-----------|-------|--------| |
| 104 | +| Token valid OR auto-refresh working | `healthy` | - | |
| 105 | +| Token expiring soon, no refresh token | `degraded` | `login` | |
| 106 | +| Token expired | `unhealthy` | `login` | |
| 107 | +| Refresh failed (after retries) | `unhealthy` | `login` | |
| 108 | +| User logged out | `unhealthy` | `login` | |
| 109 | + |
| 110 | +**Key distinction:** |
| 111 | +- **Degraded** = works now but will break soon without action |
| 112 | +- **Unhealthy** = broken, can't use until fixed |
| 113 | + |
| 114 | +## Interface Display |
| 115 | + |
| 116 | +Each interface renders `HealthStatus` consistently but adapted to its medium. |
| 117 | + |
| 118 | +### CLI |
| 119 | + |
| 120 | +**`mcpproxy upstream list` and `mcpproxy auth status`:** |
| 121 | +``` |
| 122 | +Server Health Action |
| 123 | +─────────────────────────────────────────────────────────────────── |
| 124 | +slack 🟢 Connected (5 tools) |
| 125 | +github 🟡 Token expiring in 45m → auth login --server=github |
| 126 | +filesystem 🔴 Connection refused → upstream restart filesystem |
| 127 | +new-server ⏸️ Quarantined → Approve in Web UI |
| 128 | +old-server ⏹️ Disabled → upstream enable old-server |
| 129 | +``` |
| 130 | + |
| 131 | +### Tray Menu |
| 132 | + |
| 133 | +``` |
| 134 | +🟢 slack |
| 135 | +🟡 github - Token expiring |
| 136 | +🔴 filesystem - Error |
| 137 | +⏸️ new-server (Quarantined) |
| 138 | +⏹️ old-server (Disabled) |
| 139 | +``` |
| 140 | + |
| 141 | +Clicking yellow/red servers opens Web UI to the relevant fix page. |
| 142 | + |
| 143 | +### Web UI |
| 144 | + |
| 145 | +| Location | Shows | Actions | |
| 146 | +|----------|-------|---------| |
| 147 | +| **Dashboard** | "X servers need attention" banner | Quick-fix buttons per server | |
| 148 | +| **ServerCard** | Colored status badge + summary | Login/Restart/Reconnect based on `action` field | |
| 149 | +| **ServerDetail** | Full health details | Same actions + logs | |
| 150 | + |
| 151 | +### Action Hint Mapping |
| 152 | + |
| 153 | +Each interface maps the `Action` field to its own UX: |
| 154 | + |
| 155 | +**CLI:** |
| 156 | +``` |
| 157 | +"login" → "auth login --server=%s" |
| 158 | +"restart" → "upstream restart %s" |
| 159 | +"enable" → "upstream enable %s" |
| 160 | +"approve" → "Approve in Web UI or config" |
| 161 | +"view_logs"→ "upstream logs %s" |
| 162 | +``` |
| 163 | + |
| 164 | +**Tray:** |
| 165 | +``` |
| 166 | +"login" → opens http://localhost:8080/ui/servers/{name}?action=login |
| 167 | +"restart" → triggers API call directly |
| 168 | +"enable" → triggers API call directly |
| 169 | +"approve" → opens http://localhost:8080/ui/servers/{name}?action=approve |
| 170 | +``` |
| 171 | + |
| 172 | +**Web UI:** |
| 173 | +``` |
| 174 | +"login" → Login button |
| 175 | +"restart" → Restart button |
| 176 | +"enable" → Enable toggle |
| 177 | +"approve" → Approve button |
| 178 | +``` |
| 179 | + |
| 180 | +## Implementation Changes |
| 181 | + |
| 182 | +**Files to modify:** |
| 183 | + |
| 184 | +| File | Change | |
| 185 | +|------|--------| |
| 186 | +| `internal/contracts/types.go` | Add `HealthStatus` struct | |
| 187 | +| `internal/runtime/runtime.go` | Calculate `Health` in `GetAllServers()` | |
| 188 | +| `internal/httpapi/server.go` | Ensure `health` field is included in API response | |
| 189 | +| `cmd/mcpproxy/upstream_cmd.go` | Update `upstream list` to use `Health` field | |
| 190 | +| `cmd/mcpproxy/auth_cmd.go` | Update `auth status` to use `Health` field | |
| 191 | +| `internal/tray/managers.go` | Update `getServerStatusDisplay()` to use `Health` field | |
| 192 | +| `frontend/src/components/ServerCard.vue` | Use `health` for badge color + show action | |
| 193 | +| `frontend/src/views/Dashboard.vue` | Use `health.level` to filter servers needing attention | |
| 194 | + |
| 195 | +**No backward compatibility needed** - all clients (CLI, tray, web) ship together in mcpproxy releases. |
| 196 | + |
| 197 | +## Architecture |
| 198 | + |
| 199 | +``` |
| 200 | +┌─────────────────────────────────────────────────────────────┐ |
| 201 | +│ Backend (Runtime) │ |
| 202 | +│ ┌─────────────────────────────────────────────────────────┐│ |
| 203 | +│ │ CalculateHealth() → HealthStatus ││ |
| 204 | +│ │ - Level: healthy/degraded/unhealthy ││ |
| 205 | +│ │ - AdminState: enabled/disabled/quarantined ││ |
| 206 | +│ │ - Summary: "Connected (5 tools)" ││ |
| 207 | +│ │ - Action: login/restart/enable/approve/"" ││ |
| 208 | +│ └─────────────────────────────────────────────────────────┘│ |
| 209 | +└─────────────────────────────────────────────────────────────┘ |
| 210 | + │ |
| 211 | + GET /api/v1/servers |
| 212 | + │ |
| 213 | + ┌───────────────────┼───────────────────┐ |
| 214 | + │ │ │ |
| 215 | + ▼ ▼ ▼ |
| 216 | + ┌─────────┐ ┌─────────┐ ┌─────────┐ |
| 217 | + │ CLI │ │ Tray │ │ Web UI │ |
| 218 | + │ │ │ │ │ │ |
| 219 | + │ 🟢/🟡/🔴 │ │ 🟢/🟡/🔴 │ │ badges │ |
| 220 | + │ + hint │ │ + click │ │ + btns │ |
| 221 | + └─────────┘ └─────────┘ └─────────┘ |
| 222 | +``` |
| 223 | + |
| 224 | +**Key principle:** Backend owns health calculation. Interfaces only render. |
| 225 | + |
| 226 | +## Success Criteria |
| 227 | + |
| 228 | +1. All three interfaces show identical health status for any server |
| 229 | +2. Yellow/red states always include actionable guidance |
| 230 | +3. OAuth token issues visible in tray and web (not just CLI) |
| 231 | +4. Admin state (disabled/quarantined) clearly distinct from health |
0 commit comments