Skip to content

Commit 27dedef

Browse files
authored
feat(connect): stat-only overall status + on-demand content reads (US1) (#706)
Related #696 Spec 075 US1. Connect overall status (GetAllStatus) now determines each client's installed state via os.Stat metadata only and performs ZERO config content reads, eliminating the macOS "wants to access data from other apps" privacy prompt storm when simply viewing status (FR-001, SC-001/SC-002). Reading a client's config contents to detect whether mcpproxy is registered now happens only on an explicit per-client action via the new GetStatus(id), which resolves Connected + AccessState (FR-002). ## Changes - Add a content-read seam: Service.readFile (default os.ReadFile) + NewServiceWithReader / setReadFile so tests inject reads/denials. - ClientStatus gains additive access_state + remediation fields (FR-006). - GetAllStatus: metadata-only existence; AccessState="unknown", Connected=false for installed clients (eager findEntry removed). - GetStatus(clientID): single on-demand content read via the seam; resolves accessible/absent/malformed and Connected. - Route all content reads (readOrCreate*, disconnect*, verifyJSONEntry, entry-finders) through the seam; entry-finders parse bytes only. - GetConnectedCount/GetConnectedIDs re-implemented to read lazily per client. - CLI `mcpproxy connect` resolves Connected on demand (explicit action), preserving the CONNECTED column. ## Testing - New TestGetAllStatus_NoContentReads (reader asserts 0 calls), TestGetStatus_ReadsSingleClientOnDemand (exactly 1 read), plus absent / malformed / unknown-client cases. - Migrated GetAllStatus connected-detection tests to GetStatus. - go test ./internal/connect/... ./internal/httpapi/... -race green; golangci-lint v2 clean; build (personal+server) ok; API E2E 65/65 pass.
1 parent 10efe4a commit 27dedef

13 files changed

Lines changed: 1021 additions & 87 deletions

File tree

cmd/mcpproxy/connect_cmd.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,19 @@ func runDisconnect(cmd *cobra.Command, args []string) error {
132132
}
133133

134134
func printConnectStatus(svc *connect.Service, formatter clioutput.OutputFormatter, format string) error {
135+
// Spec 075: GetAllStatus is content-read-free and leaves Connected/AccessState
136+
// unresolved. Running `mcpproxy connect` is an explicit user action, so resolve
137+
// each supported+installed client's connected state on demand via GetStatus to
138+
// preserve the CONNECTED column. Unsupported/absent clients keep the cheap
139+
// metadata-only listing (no content read).
135140
statuses := svc.GetAllStatus()
141+
for i := range statuses {
142+
if statuses[i].Supported && statuses[i].Exists {
143+
if st, err := svc.GetStatus(statuses[i].ID); err == nil {
144+
statuses[i] = st
145+
}
146+
}
147+
}
136148

137149
if format == "table" {
138150
headers := []string{"CLIENT", "STATUS", "CONFIG PATH", "CONNECTED"}

docs/api/rest-api.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,32 @@ Success returns `data.server` (`name`, `protocol`, `command`, `args`, `url`,
634634
Drop a registry's cached server lists. Returns
635635
`{ "registry_id": "...", "cleared": <n> }`.
636636

637+
### Connect (client wizard)
638+
639+
#### GET /api/v1/connect
640+
641+
Lists the connection status of every known MCP client (Claude Desktop, Cursor,
642+
VS Code, Codex, Gemini, OpenCode, …).
643+
644+
As of Spec 075, the overall listing determines each client's installed state
645+
using **file-existence metadata only** (`os.Stat`) and performs **zero config
646+
content reads** — so simply viewing status never triggers the macOS
647+
"wants to access data from other apps" privacy prompt. Each per-client object is
648+
additive-compatible and gains two fields:
649+
650+
| Field | Type | Meaning |
651+
|-------|------|---------|
652+
| `exists` | bool | Config file present (metadata only). |
653+
| `connected` | bool | mcpproxy registered in the config. Authoritative **only** when `access_state == "accessible"`; `false`/unresolved in the overall listing. |
654+
| `access_state` | string | `"unknown"` in the overall listing (not content-checked); resolved to `"accessible"`, `"absent"`, or `"malformed"` by an on-demand single-client read. |
655+
| `remediation` | string | Present only when access is denied (populated by later stories). |
656+
657+
A client that is installed but not yet content-checked reads as
658+
`exists=true, connected=false, access_state="unknown"`. Resolving `connected`
659+
requires an explicit per-client read (connect/disconnect, or the CLI
660+
`mcpproxy connect` command), which is the only place a privacy prompt may
661+
legitimately appear.
662+
637663
### Real-time Updates
638664

639665
#### GET /events

0 commit comments

Comments
 (0)