Skip to content

spec(039): Security Scanner Plugin System#356

Open
Dumbris wants to merge 66 commits intomainfrom
039-security-scanner-plugins
Open

spec(039): Security Scanner Plugin System#356
Dumbris wants to merge 66 commits intomainfrom
039-security-scanner-plugins

Conversation

@Dumbris
Copy link
Copy Markdown
Member

@Dumbris Dumbris commented Mar 28, 2026

Summary

  • Universal MCP security gateway with pluggable scanner architecture
  • Plugin-only design: Cisco, Snyk, Ramparts, MCPScan as Docker-based plugins
  • Three scanner input types: source filesystem, MCP connection, container image
  • SARIF output standard
  • Four-phase container lifecycle: Install → Scan → Approve → Runtime
  • Frozen snapshots, read-only runtime, integrity verification on restart
  • Scanner marketplace UX with one-click install
  • Multi-UI support: REST API + SSE for Web UI, CLI, macOS tray

Test plan

  • Review spec for completeness
  • Validate API design against existing MCPProxy patterns
  • Check container lifecycle against Docker best practices
  • Review data model for BBolt storage compatibility

🤖 Generated with Claude Code

claude added 30 commits March 23, 2026 19:31
Spec A covering tray menu, core process management, notifications,
Sparkle auto-update, symlink setup, and tray icon badges.

6 user stories, 25 functional requirements, 10 success criteria.
Scope bounded: main window (Spec B) and testing (Spec C) deferred.
Phase 0: Research on MenuBarExtra, Unix socket transport, Sparkle,
SMAppService, UNUserNotificationCenter, Process management, SSE.

Phase 1: Data model, API contracts consumed, quickstart guide.
All constitution checks pass. No violations.
Complete implementation of MCPProxy native macOS tray app (Spec A):

## Source Files (14 files, ~3,700 lines)
- MCPProxyApp.swift: @main with MenuBarExtra scene
- Core/CoreState.swift: 6-state machine with error mapping
- Core/CoreProcessManager.swift: Actor managing mcpproxy serve lifecycle
- Core/SocketTransport.swift: Unix socket URLProtocol for API calls
- API/APIClient.swift: Async/await REST client
- API/Models.swift: Codable types matching Go API responses
- API/SSEClient.swift: Server-Sent Events consumer via AsyncStream
- Menu/TrayMenu.swift: Full menu with server submenus, alerts
- Menu/TrayIcon.swift: Health-based tray icon
- Services/NotificationService.swift: Rate-limited macOS notifications
- Services/AutoStartService.swift: SMAppService login item
- Services/SymlinkService.swift: /usr/local/bin symlink management
- Services/UpdateService.swift: Sparkle 2.x auto-update wrapper
- State/AppState.swift: @observable root state

## Tests (4 files, ~2,150 lines)
- CoreStateTests, ModelsTests, SSEParserTests, NotificationRateLimitTests

## Build Infrastructure
- Package.swift, Info.plist, entitlements, build-macos-tray.sh
- Change AppState from @observable to ObservableObject (macOS 13 compat)
- Change @State to @StateObject/@ObservedObject for ObservableObject
- Add Error conformance to CoreError enum
- App now compiles to valid 1.3MB arm64 binary with CLT-only toolchain

Verified: swiftc full compilation produces working Mach-O executable.
Tests require Xcode.app for XCTest framework (syntax-validated only).
- Add NSLocalNetworkUsageDescription to explain the local network prompt
- Remove network.server entitlement (tray only connects, doesn't listen)
Root cause: .task{} on MenuBarExtra with .menu style only runs when
the user clicks the tray icon. Core process never started until then.

Fix: Move core launch to NSApplicationDelegateAdaptor.applicationDidFinishLaunching().
This ensures the core starts immediately on app launch.

Also:
- Fix /healthz/ready -> /ready endpoint path
- Make socket the unconditional default transport (not gated on isSocketAvailable at init)
- Expose managedProcess for synchronous shutdown in applicationWillTerminate
A stale socket file from a killed core would trick the tray into
attaching to a dead process instead of launching a new one.

Now: probe with an actual /ready API call. If it fails, remove the
stale socket and launch a fresh core subprocess.
Root cause: SocketURLProtocol reads until EOF before delivering data.
SSE is an infinite stream that never sends EOF, so the URLProtocol
hangs forever and URLSession times out with "The request timed out."

Fix: SSE always uses TCP (127.0.0.1:8080) which supports real streaming
via URLSession.bytes(for:). Regular API calls continue using Unix socket.
The core listens on both transport layers simultaneously.
Root cause: Go's HTTP server over Unix socket does NOT close the
connection after sending a response with Connection: close header.
The SocketURLProtocol read loop waited for EOF that never came,
causing URLSession to time out after 30s.

Fix: Rewrite readResponse() to parse headers first, extract
Content-Length, then read exactly that many body bytes instead of
reading until EOF. Handles both Content-Length and chunked encoding.

Verified: connectToCore() now completes in ~28ms (was timing out).
Socket API calls and TCP SSE streaming both work correctly.
Root cause: SwiftUI's MenuBarExtra with .menu style uses NSMenu
under the hood. ForEach over @published arrays appends to the
NSMenu on each re-render instead of replacing items.

Fix: Add .id(menuIdentity) modifier on the menu content that
changes when the server list or counts change, forcing SwiftUI
to tear down and rebuild the entire menu tree.
Root cause: Every SSE status event (fires every few seconds) triggered
refreshServers() which set @published servers array, causing SwiftUI
to re-render MenuBarExtra. The .menu style MenuBarExtra appends to
the underlying NSMenu instead of diffing, creating duplicates.

Three-part fix:
1. SSE status events now parse inline counters instead of re-fetching
   the full server list. Only servers.changed triggers a fetch.
2. AppState.updateServers() only publishes when data actually changes
   (compares server IDs, connected count, tool count before setting).
3. Remove .id() workaround which didn't help with .menu style.
SwiftUI's MenuBarExtra with .menu style has a fundamental bug where
ForEach over @published arrays appends to the underlying NSMenu on
each re-render instead of diffing/replacing items. This caused every
server to appear N times (once per SSE-triggered state update).

Fix: Replace the entire SwiftUI menu with pure AppKit:
- NSStatusItem for the tray icon
- NSMenu rebuilt from scratch on debounced state changes (500ms)
- Combine subscriber on appState.objectWillChange triggers rebuild
- Menu actions use @objc selectors with representedObject for context

This is the "AppKit escape hatch" approach from the original design.
SwiftUI is retained for the future main window (Spec B).
Spec C: Swift binary exposing macOS Accessibility API as MCP tools.
6 tools: list_menu_items, click_menu_item, read_status_bar,
check_accessibility, list_running_apps.
Works with any macOS app, defaults to MCPProxy.
Swift binary that exposes macOS Accessibility API as MCP tools over
stdio (JSON-RPC 2.0). 5 tools for automated UI testing:

- check_accessibility: verify AX API permission
- list_running_apps: find apps by bundle ID
- list_menu_items: read tray menu tree (opens menu via CGEvent click)
- click_menu_item: trigger menu actions by path
- read_status_bar: read status item title/position/description

Uses CGEvent for reliable menu opening (AXPress doesn't work for
NSStatusItem menus). Closes menus via Escape key event.

Verified against running MCPProxy.app — successfully reads full
menu tree with 23 servers, submenus, and all action items.

Binary: 193KB, compiles with swiftc (no SPM needed).
Tested with mcpproxy-ui-test MCP server. Fixes:

1. Attention section: show action context next to server name
   "imagegen — Disabled", "supabase — Authentication required"
   Header now shows count: "Needs Attention (9)"

2. Activity section: deduplicate entries by server:tool:type key,
   add relative timestamps ("4m ago", "just now")

3. OAuth servers: show "Log In" button in submenu when auth required

4. Server status: show "Connecting..." for servers still connecting
1. Health badge: NSStatusItem icon now shows colored dot
   (green=healthy, yellow=degraded, red=error, gray=disconnected)
   with white outline for visibility on any menu bar background.

2. Notification triggers: SSE events now trigger macOS notifications
   for new quarantine events and sensitive data detections.
   Compares before/after counts to avoid duplicate alerts.
Replace Sparkle stub with working GitHub Releases API checker.
Checks github.com/smart-mcp-proxy/mcpproxy-go/releases/latest,
compares versions, shows update in menu with download link.

Sparkle SPM integration deferred until Xcode is available for
full auto-update UX (download, verify, replace, relaunch).
SwiftUI main window opened from tray menu "Open MCPProxy..." (Cmd+,):

Views:
- MainWindow: NavigationSplitView with sidebar (Servers, Activity, Tokens, Config)
- ServersView: Server list with health dots, tool counts, action menus
- ActivityView: HSplitView with filterable list + detail panel
- TokensView: Agent token management with create/revoke
- ConfigView: JSON config viewer/editor with validation

Window management:
- NSWindow hosting SwiftUI content via NSHostingView
- Remembers position (setFrameAutosaveName)
- Async APIClient wiring (actor-isolated)
- Single instance — reopens on subsequent clicks

Also added fetchRaw/postRaw/deleteAction to APIClient for custom endpoints.
1. Duplicates in ServersView: Use @State snapshot of servers instead
   of binding directly to @ObservedObject. List only updates when
   server IDs change, not on every SSE status event.

2. Cmd+Tab: Set NSApp.setActivationPolicy(.regular) when main window
   opens, .accessory when it closes. App now appears in Dock and
   Cmd+Tab switcher while the window is visible.

3. Sidebar navigation: Add .tag() to ForEach items in NavigationSplitView
   and use .listStyle(.sidebar). Selection binding now works.

4. Web UI URL: Include API key in URL (?apikey=...) so the core
   authenticates the browser session. Key read from CoreProcessManager.

All verified via mcpproxy-ui-test MCP tools.
1. Sidebar: Agent Tokens → Secrets (matching Web UI)
2. SecretsView: Keyring secrets + env vars with add/delete,
   filter by type (keyring/env/missing), search, stat badges
3. Web UI: Updated bundled binary with make build (was serving
   blank page from dev binary without embedded frontend)
4. ServersView: @State snapshot prevents duplicates
SwiftUI List with @ObservedObject/@published has an unfixable bug
where it appends duplicate rows on each re-render instead of diffing.
ScrollView + LazyVStack does not have this issue.

Verified: tray menu shows 23 unique servers after 3+ minutes of
continuous SSE events (MCP tool verification).
1. Servers: moved apiClient into AppState as @published property.
   Window created once, views read appState.apiClient reactively.
   CoreProcessManager sets appState.apiClient on connect.
   No more NSHostingView replacement — single view tree.

2. Secrets: fixed endpoint /api/v1/secrets -> /api/v1/secrets/config.
   New models (ConfigSecret, SecretRefInfo, ConfigSecretsResponse)
   match the actual API response with secret_ref objects.

3. Activity: added summary stats bar (Total/Success/Errors/Blocked),
   Type/Server/Status filter Pickers with server-side query params,
   human-friendly type names (tool_call -> Tool Call).

All views now read apiClient from appState for reactive updates.
ScrollView+LazyVStack had layout issues inside NavigationSplitView
detail area — only 1 server rendered despite 23 in appState.

Fix: Use List with .id() keyed on server count + first server ID.
This forces SwiftUI to tear down and rebuild the List when the
server set changes, avoiding both:
- The duplication bug (List only builds once per .id() value)
- The layout issue (List properly fills the detail area)
…elegate, Equatable rows, activation policy

Research-driven improvements:

1. Accessibility identifiers: Added .accessibilityIdentifier() to all
   SwiftUI views (sidebar, servers list, activity filters, secrets,
   config). Required for MCP-based UI testing of window content.

2. NSMenuDelegate.menuWillOpen: Menu rebuilds on-demand when user
   clicks tray icon (always fresh). Also rebuilds on debounced state
   changes for background updates. In-place rebuild (removeAllItems)
   instead of replacing the NSMenu object.

3. ServerRow: Equatable: Added Equatable conformance + .equatable()
   modifier. SwiftUI skips re-rendering rows whose server data
   hasn't changed — biggest performance win for List with SSE updates.

4. Activation policy dance: .prohibited in willFinishLaunching
   (prevents focus steal), .accessory in didFinishLaunching,
   .regular before makeKeyAndOrderFront, .accessory on windowWillClose.

Verified: MCP tools show 23 unique servers, all menu actions work,
main window opens, Web UI opens with correct API key.
DEFINITIVE FIX for the List duplication bug. SwiftUI List with
@ObservedObject/@published arrays is fundamentally broken — every
objectWillChange signal causes the List to append duplicate rows.

Tried and failed:
- List with .id() — duplicates when content updates
- ScrollView+LazyVStack — doesn't fill NavigationSplitView
- @State local copy — parent re-evaluates, creates new view
- ServersListContainer wrapper — same recreation issue

Solution: AppKit NSTableView via NSViewRepresentable.
- NSTableView has zero duplication — it's a mature AppKit component
- Uses NSTableViewDataSource/Delegate pattern
- View recycling via makeView(withIdentifier:)
- Updates via reloadData() from @binding
- Servers loaded from API into @State on appear
- Health dots, name, status, protocol badge, tool count

Verified: 20+ seconds of continuous SSE events, zero duplicates.
…attention filter

1. Servers moved to submenu: "Servers (23)" → click to expand full list.
   Reduces main menu clutter from 23 flat items to 1 expandable item.

2. Needs Attention: no longer shows intentionally disabled servers.
   Only shows servers needing action: auth required, connection errors,
   quarantine approval. "enable" action is excluded from the filter.

3. Auth indicators: Servers requiring OAuth show:
   - Red dot overlay on the status icon
   - "Log In (Opens Browser)" as first action in submenu
   - "Authentication required" as status text

4. Server submenu enriched: shows protocol type, status with tool count,
   actions (Login, Enable/Disable, Restart, View Logs).

5. Fixed duplicate tools text: "Connected (57 tools) (57 tools)" → "Connected (57 tools)"

Verified via MCP list_menu_items: 23 unique servers in submenu,
auth servers show login action, disabled servers excluded from attention.
1. Use server.name instead of server.id for API calls — the Go API
   expects the server name in the URL path, and server.id is empty
   in the API response.

2. Use appState.apiClient directly instead of async coreManager.apiClientForActions
   which could return nil from the async actor context.

3. Add periodic server refresh (every 10s) to keep health/action data
   current in the menu. Previously only SSE counters were updated.

4. menuWillOpen now fetches fresh server data before rebuilding.

5. Added NSLog debugging for login flow.

Verified: cloudflare-graphql "Log In (Opens Browser)" clicked →
"API call succeeded" → browser opens with OAuth page.
6 user stories covering critical gaps found in QA:
P1: Server detail view (tools/logs/config), Add/Import server dialog,
    Tool quarantine approval workflow
P2: Server actions in main window (right-click, double-click),
    Action feedback (toast), Tool search (BM25)

No backend changes needed — all APIs already exist.
claude added 29 commits March 27, 2026 08:05
…enu, tool diff

1. Cmd+Tab app icon from bundled icon-128.png
2. Pure black template tray icon (isTemplate=true)
3. State-based icon: plain=running, pause=paused, warning=error
4. Error reason as first menu item + dashboard banner
5. Pause/Resume core process
6. Simplified menu: removed activity, config, logs items
7. Tool diff disclosure on pending/changed tools
1. Pause/Resume: SIGTERM kills core process, handleProcessExit checks
   isPaused flag to prevent auto-retry. Resume creates fresh
   CoreProcessManager. Verified: full pause/resume cycle works.

2. Tray icon overlay: paused/error badges drawn as small overlay on
   the MCPProxy base icon (bottom-right corner), not full replacement.

3. Status dot on menu header: green=healthy, yellow=connecting/attention,
   red=error, gray=paused. 10px colored circle as NSImage on title item.

4. Tool diff: fixed field name mismatch (previous_*/current_* vs old_*/new_*)
   for the API response. View Changes disclosure shows colored diffs.
1. Icon: always use isTemplate=true (pure black, adapts to menu bar).
   State indicators shown as text next to icon (⏸ for paused, ⚠ for error)
   instead of composited colored overlay that caused grayscale rendering.

2. Menu labels: "Pause MCPProxy Core" / "Resume MCPProxy Core"

3. Pause: checks isPaused in handleProcessExit to prevent auto-retry.
   SIGTERM kills core, no new process spawns.

Verified: full pause/resume cycle, icon stays black template,
⏸ appears when paused, disappears on resume.
… hash migration

Root cause: JSON schema key ordering is non-deterministic across MCP
server reconnections. Previously stored hashes were computed with one
key order, but reconnections produce a different order → hash mismatch
→ false "tool_description_changed" events.

Three-layer fix:
1. Normalize JSON schemas before hashing (parse → json.Marshal with
   sorted keys). All new hashes are stable regardless of key order.
2. Content comparison fallback: if hash mismatches but normalized
   description + schema are semantically identical, auto-approve
   silently without emitting an activity event.
3. "Changed" status restoration: tools falsely flagged as "changed"
   by previous sessions are automatically restored to "approved"
   when their description matches on next check.

Self-healing: existing DB records are migrated on-the-fly as tools
are encountered. No manual migration needed. First run after upgrade
silently fixes all stale hashes.

Verified: zero quarantine events in current session after multiple
pause/resume cycles. Historical events from earlier sessions remain
in the activity log but no new false positives are generated.
- Add JSONValue enum for type-safe dynamic JSON decoding
- Enrich ActivityEntry with arguments, response, metadata fields
- Add intent helper computed properties (operationType, reason, sensitivity)
- SSE live updates via activityVersion counter bumped on activity events
- Dynamic timestamp updates every 20s using TimelineView
- Intent badges (read/write/destructive) and reason text in list rows
- Rich detail view: colored JSON for request args and response body
- Intent Declaration section with operation badge and sensitivity
- Additional Details section for remaining metadata
- Export button (JSON/CSV) with NSSavePanel and current filter support
- Type-checks clean with zero errors

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add two new MCP tools for visual verification:
- screenshot_window: capture app window or full screen via CGWindowListCreateImage
- screenshot_status_bar_menu: open tray menu, capture, and close

Uses CGDisplayCreateImage for full screen and CGWindowListCreateImage for
per-window capture. Supports output_path for file save or base64 inline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Build commands for Swift tray app and UI test tool
- mcpproxy-ui-test MCP server tool reference (7 tools including screenshots)
- Post-change verification workflow
- MCP config example for Claude Code integration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- screenshot_status_bar_menu: use osascript screencapture with fallback,
  document Screen Recording TCC requirement for menu screenshots
- navigateToSubmenu: always press/hover item before reading children
  to trigger macOS lazy AXMenu population
- Prefix matching for submenu paths ("Servers" matches "Servers (24)")
- Increased submenu wait from 0.15s to 0.5s for large menus
- Fix CLAUDE.md: /api/v1/tools -> /api/v1/index/search (correct endpoint)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixed issues:
- Quarantine false positives: rebuilt core with fresh binary (was old binary hash corruption)
- Tool search endpoint: documented correct path /api/v1/index/search
- UI test submenu: hover-before-read, prefix matching for large menus
- Window test results: verified via screenshots and AXRow selection

Remaining known limitations (SKIP):
- T08: Menu screenshot needs Screen Recording TCC permission
- T27/T28: Config/Secrets view navigation blocked by SwiftUI AX limitation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
10 test categories, 100 scenarios executed by 10 parallel agents:
- REST API: 10/10 PASS
- Tool Discovery: 9/10 PASS
- Tool Execution: 10/10 PASS
- Security: 10/10 PASS
- Activity Log: 8/10 PASS
- Server Management: 5/10 PASS
- Tray Menu: 9/10 PASS
- Code Execution: 10/10 PASS
- Edge Cases: 10/10 PASS
- CLI/Config: 5/10 PASS

Critical findings:
- Server disable/enable/restart API hangs (lock contention in lifecycle.go)
- Activity export ignores limit param (streams all 46MB)
- Time range filter causes server deadlock
- CLI commands fail when server endpoints are blocked

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Critical fixes for issues found in 100-scenario QA:

Storage deadlock: Remove redundant RWMutex from activity storage
EnableServer hang: Make LoadConfiguredServers async
Activity export: Respect limit parameter (was streaming entire DB)
Annotation filter: readOnlyHint=true implies not destructive
Server id field: Populate from name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 14 previously-failing tests now pass after fixes:
- Storage deadlock: FIXED (RWMutex removed)
- Enable/disable hang: FIXED (async LoadConfiguredServers)
- Export pagination: FIXED (respects limit param)
- Time range deadlock: FIXED (no global lock)
- Concurrent requests: FIXED (all 10 return 200)
- CLI commands: FIXED (endpoints responsive)
- Annotation filter: FIXED (readOnlyHint respected)
- Server id field: FIXED (populated from name)

4 SKIPs: version header (dev build), 3 batch3 degraded (MCP session)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add serversVersion counter to AppState (bumped on servers.changed
  and config.reloaded SSE events)
- ServersView: add onChange(of: serversVersion) to auto-reload
- CoreProcessManager: bump serversVersion on servers.changed,
  bump both counters on config.reloaded
- AppState.updateServers: always update server array (was skipping
  when only health/status changed, causing stale display)

Dashboard auto-refreshes via @ObservedObject binding to appState
counters. Activity Log already had activityVersion watcher.

Verified: tool call triggers activity → dashboard updates without
manual refresh click.

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

- Add ServerLogEntry model for structured log objects (timestamp, level, message)
- ServerLogsResponse now handles both `logs` (structured) and `lines` (plain) formats
- APIClient.serverLogs() uses displayLines to resolve either format
- Logs tab: color-coded levels (red=ERROR, orange=WARN, gray=DEBUG)
- Auto-scroll to bottom on new log lines via ScrollViewReader
- Line count shown in header

Root cause: API returns `logs: [{timestamp, level, message}]` but Swift
model expected `lines: [String]`, silently returning empty array.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: APIClient.disableServer() was calling POST /enable with
{"enabled":false} body, but the API has separate /enable and /disable
endpoints that ignore the body. Disable was silently calling enable.

- disableServer() now calls POST /servers/{id}/disable
- enableServer() simplified (no body needed)
- CLAUDE.md: document separate enable/disable endpoints

Verified: tray menu Servers > server > Disable works correctly.
Full cycle: Disable → enabled=False, disconnected → Enable →
enabled=True, connected=True, 13 tools, healthy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Servers table (ServersView.swift):
- Redesigned from single-column list to 7-column NSTableView
- Columns: Status dot, Name, Type, Status text, Tools, Token Size, Actions
- Action buttons per row: Play/Stop toggle, Restart, Info (detail), Delete
- Delete shows NSAlert confirmation dialog
- Color-coded status: green=connected, orange=quarantined, gray=disabled
- Token size formatted as K/M units
- Column headers with auto-resizing name column
- Right-click context menu with all server actions
- onServersChanged callback refreshes list after actions

Server detail tabs (ServerDetailView.swift):
- Tab bar click area enlarged: minWidth 80pt, padding 24/10
- contentShape(Rectangle()) makes full padded area clickable
- Selected tab has visible background + border overlay

Server logs (ServerDetailView.swift):
- Removed horizontal scroll — logs wrap within available width
- fixedSize(horizontal: false, vertical: true) prevents sidebar overlap
- lineLimit(nil) allows unlimited wrapping

API (APIClient.swift):
- Added deleteServer() via DELETE /api/v1/servers/{id}

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tool detail view (ServerDetailView.swift):
- Expandable disclosure rows: click chevron to expand/collapse
- Collapsed: tool name (monospaced), one-line description, annotation badges
- Expanded: full description, all annotations as colored badges
  (readOnly=green, destructive=red, idempotent=blue, openWorld=orange),
  approval status with colored dot
- FlowLayout for wrapping annotation badges horizontally

Server table sorting (ServersView.swift):
- All data columns are sortable (click header to toggle asc/desc)
- Sort indicator (triangle) shown on active sort column
- Default sort: name ascending (stable alphabetical order)
- Uses sortedServers computed property everywhere (data source + actions)
- Stable sort prevents server "jumping" after enable/disable toggle
- State ordering: connected < disconnected < unhealthy < quarantined < disabled

Verified: disable/enable preserves alphabetical sort order.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dashboard (DashboardView.swift):
- Stats cards with subtitles (enabled count, percentage, "all clear")
- Token Savings section: saved tokens (green), full list size, query result
- Token Distribution: horizontal bar chart of top 6 servers by token size
- Recent Sessions table: client, status badge, tool calls, started time
  (derived from activity grouped by sessionId)
- Recent Tool Calls table: time, server, tool, status badge, duration, intent
  (replaces old simple activity dot list)

Activity Log (ActivityView.swift):
- Proper tabular layout with column headers
- Columns: Time, Type, Server, Details, Intent, Status, Duration
- Fixed-width columns with alignment
- Status badges (Success/green, Error/red, Blocked/orange)
- Intent badges (read/write/destructive)
- Minimum width increased to 560pt for columns
- Preserved: HSplitView detail panel, filters, SSE live updates,
  TimelineView timestamps, export button, search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fontScale property on AppState (persisted in UserDefaults)
- NSEvent local monitor intercepts Cmd+/Cmd-/Cmd+0 keyboard shortcuts
- scaleEffect applied to detail view content for consistent zoom
- View menu items: Make Text Bigger, Make Text Smaller, Actual Size
- Edit menu added (Cmd+C copy, Cmd+A select all)
- Menu injected into system menu bar via applicationDidBecomeActive
- Scale range: 0.6x to 2.0x, default 1.0x, step 0.1x

Verified: 3x Cmd+ zooms content visibly (stats cards, tables, text).
Cmd+0 resets to default. Scale persists across app restarts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: serverStatusColor() checked health.level before checking
server.enabled. Disabled servers with cached health="healthy" showed
green dots instead of gray.

Fix: check !server.enabled FIRST, return .systemGray before any
health checks. Same priority order as the main window table.

Also added: macOS design guide document (docs/superpowers/specs/)
from HIG research + code audit findings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Centralized health colors:
- Added statusColor (SwiftUI) and statusNSColor (AppKit) to ServerStatus
- Removed 3 duplicate color-logic blocks across views and tray menu

Sidebar (MainWindow.swift):
- Replaced colored icon badges with native monochrome SF Symbols
- Let system handle sidebar item styling and selection highlight
- Removed SidebarItem.color property

Typography (all views):
- Replaced hardcoded font sizes with text styles (.title, .body,
  .subheadline, .caption, .caption2)
- NSTableView cells use NSFont.systemFontSize / .smallSystemFontSize

Spacing & corners (all views):
- Standardized padding to 8pt grid (4/8/16/20)
- Standardized corner radius to 8pt (was mix of 6/8/10)

Colors & accessibility (all views):
- Replaced white-on-red with semantic red-on-transparent patterns
- Fixed .opacity(0.5) backgrounds → solid Color(.controlBackgroundColor)
- JSON syntax: .green→.teal (strings), .cyan→.blue (keys) for dark mode
- Badge shapes: Capsule() consistently
- Added .accessibilityLabel() to status dots, badges, action buttons
- Added .accessibilityElement(children: .combine) to stat cards

Table (ServersView.swift):
- Row height: 36→28pt (macOS standard)
- Intercell spacing: 8→12pt

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

1. Zoom (Cmd+/Cmd-): Changed from scaleEffect (scaled everything) to
   dynamicTypeSize environment (scales only text). Panels and tables
   always fill available space regardless of zoom level.

2. Tray menu pause/resume: Icons changed to .fill variants
   (pause.circle.fill, play.circle.fill) at 18x18pt for visibility.

3. Core status banner: Orange banner at top of main window when core
   is paused, red when stopped/error. Shows status text + Resume/Start
   button. ServersView and ActivityView show "Core is paused/not running"
   instead of confusing "No servers"/"No activity" messages.

4. Secrets page: Delete (trash) icon now red via .foregroundStyle(.red).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zoom (Cmd+/Cmd-/Cmd+0):
- Fixed: was using scaleEffect (did nothing after dynamicTypeSize switch)
- NSEvent monitor now correctly matches "=" key for Cmd+=
- dynamicTypeSize environment scales text only, layout fills space
- Added send_keypress tool to mcpproxy-ui-test for keyboard testing

Core terminology (Pause/Resume → Stop/Start):
- Tray menu: "Stop MCPProxy Core" / "Start MCPProxy Core"
- Banner: "MCPProxy Core is stopped" with Start button
- isPaused → isStopped across AppState, CoreProcessManager, all views
- Icons: stop.circle.fill / play.circle.fill

Server action labels (protocol-aware):
- stdio servers: "Stop" (enabled) / "Start" (disabled) — local processes
- http/sse servers: "Disable" (enabled) / "Enable" (disabled) — remote
- Applied in: tray menu submenus, server table action buttons,
  server detail header, context menus
- Helper: serverActionLabel(for:enabled:) on ServerStatus

mcpproxy-ui-test:
- Added send_keypress tool for keyboard shortcut testing
- Supports modifiers: cmd, shift, alt, ctrl (e.g., "cmd+=", "cmd+0")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: The zoom fix (GeometryReader + scaleEffect with divided
frame size) broke NSTableView (empty servers), HSplitView (activity
log sidebar overlap), and SwiftUI Lists (empty secrets). These views
depend on their parent's actual frame size to render correctly.

Fix: Remove GeometryReader and scaleEffect entirely. The Cmd+/Cmd-
keyboard shortcuts still work (fontScale is stored) but zoom is
deferred until a proper macOS text scaling approach is implemented.
All 5 pages now render correctly with full data.

Verified via mcpproxy-ui-test screenshots:
- Dashboard: 23 servers, sessions table, tool calls
- Servers: 23 rows with all columns and action buttons
- Activity Log: full table, no sidebar overlap
- Secrets: 6 keyring entries with red delete icons
- Configuration: JSON config with syntax highlighting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous approaches failed:
- scaleEffect: broke NSTableView, HSplitView, SwiftUI Lists
- DynamicTypeSize: no effect on macOS (iOS-only)
- GeometryReader+scaleEffect: same layout breakage

Working approach: setBoundsSize on the window's contentView.
When bounds are smaller than frame, AppKit automatically scales
the content up — like browser zoom. NSTableView, HSplitView,
and all child views handle this correctly because it's the
standard AppKit magnification mechanism.

Verified: 3x Cmd+= zooms dashboard (text + cards + tables larger),
servers table still shows all 23 rows with action buttons,
Cmd+0 resets. No layout breakage.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous approaches (scaleEffect, setBoundsSize, DynamicTypeSize) all
either broke layout or had no effect on macOS.

Working solution: Custom FontScaleKey environment + scaled font helpers.
Every .font() call across all 11 view files replaced with
.font(.scaled(.body, scale: fontScale)) pattern. NSTableView cells
also scale via fontScale property on Coordinator.

Layout (panels, columns, sidebar) always fills available space.
Only text size changes with Cmd+/Cmd-/Cmd+0.

New in Models.swift:
- FontScaleKey EnvironmentKey
- Font.scaled(_:scale:) for standard text styles
- Font.scaledMonospaced(_:scale:) for code/JSON
- Font.scaledMonospacedDigit(_:scale:) for numbers

Updated views (all .font() calls):
- DashboardView, ActivityView, ServerDetailView, ServersView
- SecretsView, ConfigView, TokensView, AddServerView, MainWindow

Verified: 3x Cmd+= scales text ~30% while 4 stat cards stay in
one row and server table columns fill width.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Universal MCP security gateway with pluggable scanner architecture.

Key design decisions:
- Plugin-only: all scanners are Docker-based plugins (Cisco, Snyk, Ramparts, MCPScan)
- Three input types: source (filesystem), mcp_connection (behavioral), container_image (deep)
- SARIF output standard with adapter shims for non-SARIF scanners
- Four-phase container lifecycle: Install → Scan → Approve → Runtime
- Frozen snapshots via docker commit, read-only runtime with tmpfs
- Integrity verification: image digest + source hash + lockfile hash on every restart
- Scanner marketplace UX: browse registry, one-click install, configure API keys
- Multi-UI: REST API + SSE events serve Web UI, CLI, and macOS tray app
- Auto-scan quarantined servers, manual scan for pre-configured

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

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 6e46ad3
Status: ✅  Deploy successful!
Preview URL: https://0c63676b.mcpproxy-docs.pages.dev
Branch Preview URL: https://039-security-scanner-plugins.mcpproxy-docs.pages.dev

View logs

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