- macOS 13.0+
- Swift 5.9+
- Xcode 15+
Use the same package as iOS. AppReveal ships as a single Swift package with iOS and macOS platform support.
.package(url: "https://github.com/UnlikeOtherAI/AppReveal.git", from: "0.10.1")Wrap the integration in #if DEBUG so there is no release-build footprint.
import AppKit
#if DEBUG
import AppReveal
#endif
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
#if DEBUG
AppReveal.start()
// Optional: register providers for deeper inspection
AppReveal.registerStateProvider(myStateContainer)
AppReveal.registerNavigationProvider(myRouter)
AppReveal.registerFeatureFlagProvider(myFeatureFlags)
AppReveal.registerNetworkObservable(myNetworkClient)
#endif
}
}WKWebView support works automatically on macOS as well. No extra bridge registration is required.
When the listener is ready, AppReveal prints a loopback URL and an authenticated session URL. You can also read them from AppReveal.url, AppReveal.sessionURL, and AppReveal.sessionToken.
AppReveal uses local network discovery in debug builds.
<key>NSLocalNetworkUsageDescription</key>
<string>AppReveal uses the local network for debug MCP connections.</string>
<key>NSBonjourServices</key>
<array>
<string>_appreveal._tcp</string>
</array>AppKit controls with accessibilityIdentifier are discovered through the NSView hierarchy. For SwiftUI controls that do not expose reliable AppKit views or accessibility identifiers, use the same explicit registration modifier as iOS:
Button("Refresh") { refresh() }
#if DEBUG
.appReveal("orders.refresh", label: "Refresh", activate: refresh)
#endifRegistered SwiftUI controls appear in get_elements with idSource: "appReveal" and in get_view_tree as SwiftUI.AppRevealElement. tap_element and tap_text call the activate: closure when provided, then fall back to the recorded frame.
Screen identity is auto-derived from controller class names. Override it only when you want a stable custom key.
#if DEBUG
extension OrdersListViewController: ScreenIdentifiable {
var screenKey: String { "orders.list" }
var screenTitle: String { "Orders" }
}
#endifsearchField.accessibilityIdentifier = "orders.search"
refreshButton.accessibilityIdentifier = "orders.refresh"# Discover the service
dns-sd -B _appreveal._tcp local.
# Check listener health. This endpoint is intentionally unauthenticated.
curl http://localhost:<port>/health
# Copy the token from the AppReveal log or AppReveal.sessionToken.
TOKEN="<session-token>"
# Initialize MCP session
curl -X POST http://localhost:<port>/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# List windows
curl -X POST http://localhost:<port>/ \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_windows","arguments":{}}}'For LAN clients, resolve the Bonjour service host/port and include the same token via Authorization: Bearer <token> or X-AppReveal-Session. If localhost works but LAN discovery or reachability does not, check Local Network permission, firewall/VPN state, and com.apple.security.network.server for sandboxed apps.
The repository includes a repeatable check for listener health, authentication, and MCP initialization. Pass the printed session URL; optionally pass the Mac's LAN hostname or IP to verify the same listener from a non-loopback address:
scripts/verify-macos-lan-mcp.sh '<AppReveal.sessionURL>' [<lan-host-or-ip>]For the strongest acceptance check, run that command from a second machine or use the Mac's LAN IP
from an Android Emulator shell. A valid run gets health 200, unauthenticated MCP 401, and
authenticated initialization 200 through the LAN address.
GET /health returns bonjourDiagnostics and lan objects. Use them when loopback works but remote LAN clients fail:
bonjour: "advertising"means mDNS published successfully.bonjour: "suppressed"means AppReveal kept the HTTP listener running but did not advertise because no usable LAN interface/path was visible. CheckbonjourDiagnostics.suppressionReason.bonjour: "retrying"or"failed"includeslastErrorandlastErrorHint;-65555is BonjourNoAuth, usually Local Network permission or missingNSBonjourServices.lan.interfaceslists the process-visible IPv4/IPv6 interfaces and marks which ones are LAN candidates.
All protocol conformance should be wrapped in #if DEBUG.
protocol StateProviding: AnyObject {
func snapshot() -> [String: AnyCodable]
}
protocol NavigationProviding: AnyObject {
var currentRoute: String { get }
var navigationStack: [String] { get }
var presentedModals: [String] { get }
}
protocol FeatureFlagProviding: AnyObject {
func allFlags() -> [String: AnyCodable]
}
protocol NetworkObservable: AnyObject {
var recentRequests: [CapturedRequest] { get }
func addObserver(_ observer: NetworkTrafficObserver)
}| Tool | Description |
|---|---|
list_windows |
List visible app windows with IDs, titles, frames, and key status |
get_menu_bar |
Read the main menu hierarchy recursively |
click_menu_item |
Invoke a menu item by title path, for example File > Save |
focus_window |
Bring a specific window to the front and make it key |
All native UI tools and all WKWebView/DOM tools accept an optional window_id parameter. Get the ID from list_windows, then pass it to target a specific window. If you omit window_id, AppReveal uses the current key window.
- All code behind
#if DEBUG-- zero production footprint - Generated per-session token required for MCP POST requests
- Health diagnostics available at
GET /health - Loopback CORS only; LAN clients should use native MCP clients or explicit headers
- Local Network, firewall/VPN, and sandbox network permissions can affect LAN reachability
- Bonjour publish failures retry automatically; advertising is suppressed when no LAN candidate is visible
- Sensitive headers (Authorization, Cookie) redacted in network capture
- Transport: NWListener (Network.framework)
- Discovery: NetService (Bonjour/mDNS) publishing
_appreveal._tcpafter listener readiness and LAN diagnostics - View hierarchy: NSView tree walking plus explicit SwiftUI
.appReveal(...)registrations - Screenshots: window snapshots
- WebView: WKWebView + evaluateJavaScript
See example/macOS/AppRevealMacExample/ for a macOS AppKit example with sidebar navigation, multi-window-aware tools, and curl verification steps.