Skip to content

feat(040): Add/Edit Server UX improvements for macOS tray app#359

Merged
Dumbris merged 17 commits into
mainfrom
040-server-ux
Mar 30, 2026
Merged

feat(040): Add/Edit Server UX improvements for macOS tray app#359
Dumbris merged 17 commits into
mainfrom
040-server-ux

Conversation

@Dumbris

@Dumbris Dumbris commented Mar 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Complete Add Server UX overhaul: larger sheet (560x560), pinned submit button, simplified protocol picker (stdio/http), inline field validation, phased connection test with error feedback from server logs
  • Editable Config tab in ServerDetailView with Save/Cancel, toggles for Enabled/Quarantined/Docker/Skip Quarantine
  • PATCH /api/v1/servers/{name} endpoint for partial server config updates
  • Quarantined servers auto-connect for tool inspection (1-hour exemption) — tools visible for review but hidden from LLMs
  • New tool approval security: new tools from trusted servers now require approval (prevents injection via new tool additions)
  • Docker Isolation defaults to ON with help hints explaining security benefits
  • "Approve Server" button in server detail + "Quarantine" button to re-quarantine
  • Tray menu "Needs Attention" items open macOS app server detail (not Web UI)
  • Cmd+N opens Add Server from main window
  • Socket POST body fix (httpBodyStream for URLProtocol subclasses)
  • Socket fd double-close crash fix (thread-safe closeSocket with NSLock)
  • MainActor.run for UI operations in async Task blocks

Test plan

  • Add server via Manual tab with validation feedback
  • Add server with duplicate name shows error
  • Retry after failure deletes old server, re-adds with updated params
  • Quarantined server auto-connects and discovers tools
  • Tools hidden from retrieve_tools (LLM), visible via inspect API
  • call_tool blocked for quarantined servers
  • Approve all tools + unquarantine (UI refreshes immediately)
  • Re-quarantine button works
  • PATCH endpoint: partial updates, 400 on empty body, 404 on not found
  • Cmd+N opens Manual tab from any view
  • Socket POST works (httpBodyStream fix)
  • No crash on tray menu attention item click
  • Go tests pass (httpapi, management, quarantine)
  • Swift builds clean

Generated with Claude Code

claude added 16 commits March 29, 2026 17:25
Backend:
- New `internal/connect/` package: register MCPProxy in AI client configs
  (Claude Code, Cursor, VS Code, Windsurf, Codex, Gemini) with backup
- REST API: GET/POST/DELETE /api/v1/connect/{client}
- CLI: `mcpproxy connect <client>`, `mcpproxy connect --list`, `mcpproxy disconnect`
- 32 unit tests covering all client formats (JSON + TOML)

Web Dashboard:
- Hub visualization: MCPProxy logo center, AI Agents left, Upstream Servers right
- Fat green connection lines with animated dots (20s interval)
- Connect modal with per-client connect/disconnect
- Security status: Docker isolation + quarantine protection badges
- Dynamic token savings from API, Activity Log link
- Fix: isServerConnected uses actual connected field, not health level
- Fix: "Connecting" servers no longer trigger attention banner (healthy, not degraded)

macOS App:
- Dashboard rewritten to match web UI hub layout
- Connect Clients sheet with live API data
- Real MCP sessions from /api/v1/sessions (deduplicated by client)
- Docker/quarantine status fetched from API
- Tab switching: Import, Activity Log, Add Server all navigate correctly
- App icon used as logo instead of SF Symbol shield

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security:
- fix(quarantine): new tools from trusted servers now require approval
  instead of auto-approve. Prevents injection via new tool additions on
  compromised servers. Only skip_quarantine=true opts out. Rug pull
  detection for changed tools unchanged. Added test.

macOS tray app:
- fix(tray): fetch API key from core via socket /api/v1/info instead of
  generating ephemeral key. Config file key and runtime key now match.
- fix(tray): secrets delete button now shows confirmation dialog
- fix(tray): token revoke button now shows confirmation dialog
- feat(tray): add Set/Update secret value sheet for missing keyring secrets
- fix(tray): Settings scene shows placeholder instead of blank EmptyView
- fix(tray): Activity Log Details column min-width prevents single-char wrap
- fix(tray): reduced fixed column widths for better table proportions

Web UI:
- fix(frontend): remove CSS transition deadlock that blocked SPA navigation
- fix(frontend): add scrollBehavior to reset scroll on route change

QA report: docs/qa/mcpproxy-qa-report-2026-03-29.html (121 tests, 92.6% pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… config, validation

## Backend (Go)
- Add PATCH /api/v1/servers/{name} for partial server config updates
- Fix command/args/url/working_dir missing from /api/v1/servers response
- Fallback to storage when stateview Config is nil
- UpdateServer on ServerController interface + implementation

## macOS Tray App (Swift)
- AddServerView: sheet 560x560, pinned submit, simplified protocols, inline validation
- AddServerView: phased connection test with Save Anyway/Retry on failure
- ServerDetailView: editable Config tab with Edit/Save/Cancel + toggles
- ServerDetailView: log auto-refresh (3s), last-error banner, color-coded lines
- ServersView: tooltip on unhealthy status, empty state onboarding
- DashboardView: accessibility labels, removed placeholder
- MCPProxyApp: Cmd+N from main window
- APIClient: updateServer() PATCH method

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Cmd+N now switches to Servers tab before opening Add Server sheet
  (sheet is hosted by ServersView, not Dashboard)
- Fixed SwiftUI @State preservation bug: replaced onAppear tab init
  with explicit State(initialValue:) + .id() force recreation
- Manual tab correctly selected for Cmd+N, Import tab for Dashboard button

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eout

When addServer fails (timeout or error), now:
- Fetches last 10 server log lines, finds ERROR/failed lines
- Shows the actual error from logs in the failure message
- Multi-line error text with up to 8 lines, selectable
- "Save Anyway" still available for timeout cases
- Retry button resets phase before resubmitting
- Helpful hint for timeout: "config may be saved, check command/URL"

This gives users actionable feedback (e.g., "npx: command not found",
"package @company/echo not found") instead of "The request timed out."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SocketURLProtocol has intermittent issues with POST requests over
Unix sockets that cause 30s timeouts. GET requests work fine.

Fix: When addServer() via socket times out, automatically retry via
a TCP APIClient. Gets the API key from core via socket GET (which works),
then POSTs via TCP with the API key. This transparent fallback means
Add Server now succeeds even when the socket POST path is slow.

Also increased error text lineLimit from 2 to 8 for better visibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
URLSession converts request.httpBody to request.httpBodyStream internally
when passing through URLProtocol subclasses. The SocketURLProtocol was
only checking request.httpBody (always nil for POST), causing the POST
body to be empty. The Go server received a POST with no body, returned
400 "name required", but the empty-body request itself went through fine.

The REAL issue: without Content-Length header (because body appeared empty),
the server waited for the body, causing the 30s socket read timeout.

Fix: In buildHTTPRequest(), check request.httpBodyStream when httpBody
is nil. Read the full stream into Data before building the HTTP request.
This is the standard pattern for URLProtocol subclasses handling POST.

The TCP fallback is kept as defense-in-depth but should no longer trigger.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SocketTransport:
- Read httpBodyStream when httpBody is nil (URLSession converts body
  to stream for URLProtocol subclasses) - root cause of POST timeouts
- Robust stream reading with error handling and EOF detection
- Content-Length header correction after stream read
- NSLog debug statements for diagnosing POST body issues

AddServerView:
- Quarantined servers treated as success (dialog closes, not error)
- SubmitPhase.success now carries quarantined flag
- Orange shield icon + "quarantined for security review" message
- TCP fallback kept as defense-in-depth for socket issues

ServersView:
- Default tab changed from .importConfig to .manual
- Cmd+N now correctly opens Manual tab

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…default ON

Backend:
- AddServer() now grants 1-hour inspection exemption for quarantined
  servers, so they connect and discover tools for user review
- Tools remain hidden from retrieve_tools (LLM) and blocked from
  call_tool — only visible via inspect endpoints for human review
- Security model verified: quarantine + exemption = connect for
  inspection only, not for LLM access

macOS tray app:
- Docker Isolation toggle defaults to ON (was OFF)
- Added help hints for all option toggles:
  - Enabled: "When disabled, server will not connect"
  - Docker Isolation: "Runs in isolated container, prevents filesystem access"
  - Quarantined: "Tools must be reviewed before AI agents can use them"
- Options sent in API request: docker_isolation, quarantined, enabled
- ServerDetailView edit mode also has toggle hints

Verified: add quarantined server → connects → 13 tools discovered →
tools hidden from LLM → tools visible for review → call_tool blocked

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace single 3s check with poll loop (3 attempts, 3s each)
- "Connecting..." no longer shown as red error — keeps polling
- Connected → success. Quarantined → success. Error → show actual error
- Still connecting after 9s → treat as success (server list shows live status)
- 409 "already exists" on retry → recognize previous save succeeded,
  skip to connection status check instead of showing error
- Both socket and TCP fallback paths handle 409 gracefully

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When user edits form fields after a failure (fixes name, command, args)
and clicks Retry, the old server is now deleted first via DELETE API,
then re-added with the current field values. This ensures:
- Updated params are applied (not stuck with old values)
- No 409 "already exists" error on retry
- User can change server name, command, args between attempts
- lastSavedServerName tracks what was saved for cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…vigation

1. approveTools sends {"approve_all":true} body (was empty → EOF error)
2. Duplicate server name shows error on first attempt (409 only treated
   as success if lastSavedServerName matches, i.e. our previous save)
3. "Approve Server" button in ServerDetailView header when quarantined
   — approves all tools + unquarantines in one click
4. Tray "Needs Attention" items open macOS app server detail view
   instead of Web UI (added .showServerDetail notification)
5. Quarantine logic verified: new tools=pending, changed tools=blocked,
   dynamic additions require approval (enforceNewTools)

E2E verified: add → connect → 13 tools → approve all → unquarantine →
connected + not quarantined + 13 tools + "Connected (13 tools)"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ionAction

The tray menu "Needs Attention" item handler called showMainWindow()
inside a Task{} block which runs on a cooperative thread pool. NSWindow
creation requires the main thread — calling it from background thread
caused SIGABRT (abort trap 6).

Fix: wrap showMainWindow() and notification posts in await MainActor.run{}
for both "approve" and "default" cases in handleAttentionAction.

Also fixed missing closing brace for Task{} block.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…button

SocketTransport:
- Race condition: stopLoading() and readResponse() both tried to close
  socketFD from different threads → EXC_GUARD crash when fd was reused
- Fix: thread-safe closeSocket() with NSLock, atomically reads and
  clears socketFD so only one thread can close it

ServerDetailView:
- Added NSLog debug statements to loadTools() for diagnosing empty state
- Added "Reload" button to empty tools state so users can manually retry
- Tools tab still shows "2 tool(s) need approval" banner from server model

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ServerDetailView:
- Server data is now @State (mutable) instead of let (immutable)
- refreshServer() fetches updated server status from API after mutations
- Approve Server button: approve tools + unquarantine + refresh → UI
  immediately shows the new state (quarantined badge disappears)
- New "Quarantine" button visible when server is NOT quarantined,
  allowing users to re-quarantine a previously approved server
- Enable/Disable/Restart actions also refresh the server state
- Added Reload button on empty tools state for manual retry

E2E verified: add → connect → approve → unquarantine (UI updates) →
re-quarantine (UI updates) → cleanup

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Mar 30, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1172ef7
Status: ✅  Deploy successful!
Preview URL: https://cae1294a.mcpproxy-docs.pages.dev
Branch Preview URL: https://040-server-ux.mcpproxy-docs.pages.dev

View logs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Dumbris Dumbris merged commit 82718be into main Mar 30, 2026
31 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants