mac-vnc-server is a macOS-only VNC/RFB server written in Swift. It captures the local Mac screen, accepts keyboard and mouse input from a VNC client, and exposes the session on a configurable TCP port.
The default setup is optimized for local testing with Apple Screen Sharing:
- bind address:
127.0.0.1 - base port:
5900 - password: generated and stored in
~/.mac-vnc-server/config.json - FPS target: adaptive
60 -> 45 -> 30 - scale:
1.0 - encoding:
auto
Use SSH tunneling or an explicit LAN bind for remote use.
- macOS 13 or newer
- Xcode / Swift toolchain compatible with
swift-tools-version: 6.3 - Screen Recording permission
- Accessibility / Post Event permission for keyboard and mouse injection
The package links macOS-native frameworks:
ScreenCaptureKitfor screen captureCoreGraphics/ApplicationServicesfor input injection and permissionsAppKitfor clipboard integrationzlibfor compressed framebuffer encodings
swift build -c releaseThe binary is produced at:
.build/release/mac-vnc-server
For an explicit Apple Silicon build:
swift build -c release --arch arm64Development builds always report:
0.0.0-development
The release workflow replaces that value with the Git tag being released, stripping a leading v. For example, release tag v1.2.3 builds a binary that reports 1.2.3.
Show the version:
./.build/release/mac-vnc-server version
./.build/release/mac-vnc-server --helpStartup, connection, and recovery information is always shown. Periodic framebuffer-update logs are shown only when the server is started with --verbose. Warnings and errors are always written to stderr.
Update an existing binary from the latest GitHub release:
./.build/release/mac-vnc-server updateThe command downloads the exact release assets, verifies the binary's SHA-256 checksum, and atomically replaces the executable that was invoked. If a newer release was installed, restart the server process to use it.
Run this once:
./.build/release/mac-vnc-server permissionsThen grant the requested permissions in macOS System Settings:
- Privacy & Security -> Screen Recording
- Privacy & Security -> Accessibility
Restart the server after granting permissions.
Check current status:
./.build/release/mac-vnc-server diagnoseIf ScreenCaptureKit reports no displays after the Mac turns the screen off, wake the display and start the server again:
./.build/release/mac-vnc-server wakeupDefault command:
./.build/release/mac-vnc-serverEquivalent explicit command:
./.build/release/mac-vnc-server run --bind 127.0.0.1 --port 5900 --fps auto --scale 1 --encoding autoBy default, the server exposes both the combined desktop and each display individually:
5900 all displays, composed as one virtual framebuffer
5901 display 1
5902 display 2
... one additional port per display
Connect with Apple Screen Sharing:
open 'vnc://127.0.0.1:5900'Password printed at startup:
VNC password: XXXXXXXX
For unattended local testing, fill the native Screen Sharing password dialog with AppleScript:
open 'vnc://127.0.0.1:5900'
sleep 2
osascript -e 'tell application "System Events" to keystroke "XXXXXXXX"' \
-e 'tell application "System Events" to key code 36'Do not store test credentials in Keychain unless you explicitly want that behavior.
Bind all interfaces:
./.build/release/mac-vnc-server --bind 0.0.0.0 --port 5900 --password '<your-password>'Or bind a specific LAN IP:
./.build/release/mac-vnc-server --bind 192.168.1.10 --port 5900 --password '<your-password>'The server refuses unauthenticated non-loopback binds by default. To disable auth for clients that support unauthenticated VNC, you must opt in explicitly:
./.build/release/mac-vnc-server --bind 0.0.0.0 --no-password --insecure-allow-no-authClassic VNC password auth is weak and limited by the protocol. For untrusted networks, prefer an SSH tunnel:
ssh -L 5900:127.0.0.1:5900 user@mac-host
open 'vnc://127.0.0.1:5900'mac-vnc-server [run] [options]
mac-vnc-server permissions
mac-vnc-server diagnose
mac-vnc-server wakeup
mac-vnc-server update
mac-vnc-server version
mac-vnc-server --help
run is optional when the first argument is a flag.
wakeup sends a short user-activity assertion with caffeinate to wake the display when ScreenCaptureKit cannot see any displays.
Options:
| Option | Default | Description |
|---|---|---|
--bind <ipv4> |
127.0.0.1 |
IPv4 address to listen on. |
--port <port> / -p <port> |
5900 |
TCP port, or base port when --display is omitted. |
--password <value> |
config file | Override the generated/configured classic VNC auth password for this run. |
--no-password |
off | Use unauthenticated VNC. Apple Screen Sharing does not accept this path. |
--insecure-allow-no-auth |
off | Required with --no-password on non-loopback binds. |
--fps <auto|1...120> |
auto |
Adaptive 60 -> 45 -> 30 target, or a fixed framebuffer update rate when an explicit number is provided. |
--scale <value> |
1.0 |
Base virtual framebuffer scale. Adaptive sessions may temporarily use 0.75 or 0.67 for compatible generic clients that advertise DesktopSize when encoding or network pressure persists. Apple Screen Sharing remains at the negotiated framebuffer size until its resize dialect is implemented. |
--encoding <auto|zrle|zlib|raw> |
auto |
Framebuffer encoding preference. |
--display <all|number> |
automatic | Display mode. Omit it to serve all displays on the base port and each display on consecutive ports. Use all for only the combined desktop, or a 1-based display number for only that display. |
--verbose |
off | Enable periodic framebuffer-update logs on stdout. |
--clipboard-sync |
off | Enable basic text clipboard synchronization with the VNC client. |
--no-adaptive |
off | Disable adaptive FPS, compression, and automatic scale changes. |
On the first authenticated run, the server generates an 8-character ASCII password and stores it in:
~/.mac-vnc-server/config.json
The directory is created with permissions 0700 and the file with 0600. The password is printed to stdout at startup. Subsequent runs reuse the same value. Use --password <value> for a one-off override, or --no-password for an explicitly unauthenticated server.
Omitting --display starts multiple listeners. With the default base port, 5900 keeps the previous combined-desktop behavior and 5901, 5902, ... expose each monitor separately:
./.build/release/mac-vnc-server
open 'vnc://127.0.0.1:5900' # all displays
open 'vnc://127.0.0.1:5901' # display 1
open 'vnc://127.0.0.1:5902' # display 2To keep a single listener with the combined desktop:
./.build/release/mac-vnc-server --display allTo serve only one monitor on the selected port:
./.build/release/mac-vnc-server --display 1 --port 5900Use diagnose to list display numbers:
./.build/release/mac-vnc-server diagnoseThe server implements the RFB handshake and core client messages:
- protocol negotiation
SecurityType Noneand classic VNC authSetPixelFormatSetEncodingsFramebufferUpdateRequestKeyEventPointerEventClientCutText
Apple Screen Sharing negotiates RFB 3.3 and requires VNC auth, so authentication is enabled by default.
Screen capture uses ScreenCaptureKit with one stream per selected display. Captured frames are stored in BGRA format and composed into a virtual framebuffer. The virtual framebuffer supports multiple displays and maps VNC coordinates back to macOS global coordinates for mouse input.
If macOS sleeps the screens while the server is running, the capture streams are rebuilt after screensDidWakeNotification, when an active SCStream reports didStopWithError, or when input arrives after a failed recovery. Recovery refreshes the shareable content and retries up to three times without closing the existing VNC session, so input handling remains connected.
When a client or network cannot consume updates quickly enough, the server keeps only the newest captured frame and drops stale frames before sending their RFB update header. Persistent Zlib/ZRLE state is transactional, so a dropped frame cannot desynchronize the stream. Client sockets use non-blocking writes with a five-second no-progress timeout; a connection that makes no write progress is closed instead of blocking the capture pipeline indefinitely. Adaptive sessions lower the output target from 60 to 45 to 30 FPS under sustained pressure and recover after sustained headroom; shared capture streams use the highest rate requested by their active clients.
Clients that advertise the standard DesktopSize pseudo-encoding can also enter a per-client scale ladder of 1.0 -> 0.75 -> 0.67 after sustained encoding or network pressure. Frames are resampled with Apple's Accelerate framework using high-quality filtering so the server reduces work and bandwidth only when needed. The session recovers one scale step after five healthy seconds. Clients without standard resize support remain at the configured scale and use FPS/backpressure adaptation instead.
--encoding auto chooses a compatible encoding based on the client:
- Apple Screen Sharing: persistent Zlib encoding (
6) - generic clients with ZRLE: ZRLE (
16) - generic clients with Zlib: Zlib (
6) - fallback: Raw (
0)
Zlib is kept as a persistent stream per VNC connection, which is required for stable compressed updates with Apple Screen Sharing. Adaptive compression prioritizes sender throughput: it uses level 1 when encoding is the bottleneck and at most level 3 when the network is the bottleneck. It does not automatically switch to high compression levels during video or animation. ZRLE uses lossless solid-color, palette, packed-palette, and run-length tile modes, selecting the smallest representation for each changed tile. Dirty regions use smaller tiles when appropriate, and framebuffer update rectangles are batched into fewer socket writes. The cursor remains composited into captured frames. This preserves the exact macOS cursor appearance; the server does not synthesize a separate RichCursor shape because ScreenCaptureKit does not expose that shape through a stable public API.
Keyboard and mouse events are injected with CGEvent.
The server processes input on the read loop and streams framebuffer updates on a separate writer queue. This prevents keyboard/mouse events from getting stuck behind frame compression or socket writes.
For Apple Screen Sharing, Alt_L / Alt_R keysyms are remapped to macOS Command because the native client sends Command that way. This enables shortcuts such as Cmd+C, Cmd+V, Cmd+W, and Cmd+Q.
Clipboard synchronization is disabled by default because the native macOS Screen Sharing client can apply incoming clipboard updates to the client's local pasteboard. Enable basic text synchronization explicitly when it is needed:
./.build/release/mac-vnc-server run --clipboard-syncThis uses NSPasteboard and classic VNC cut text messages; full extended clipboard support is not implemented yet.
This repository includes two workflows:
.github/workflows/ci.yml
Runs on pull requests and pushes to main:
swift testswift build -c release
.github/workflows/release.yml
Runs when a GitHub Release is published:
- replaces
0.0.0-developmentinAppVersion.swiftwith the release tag - runs tests
- builds an arm64 macOS release binary
- prepares the binary plus SHA-256 checksum
- uploads
mac-vnc-serverandmac-vnc-server.sha256to the GitHub Release
The generated password is printed on server startup:
VNC password: XXXXXXXX
For scripted testing, use AppleScript to type it instead of Keychain.
Make sure you are running a build with the split reader/writer architecture. Rebuild:
swift build -c releaseThen restart the server.
Use the default encoding first:
./.build/release/mac-vnc-server --encoding autoIf the display slept while the server was running, wait briefly for the automatic ScreenCaptureKit recovery. If the display is still unavailable, the next keyboard or mouse event sends a caffeinate wake signal and triggers another asynchronous recovery attempt. The server logs ScreenCaptureKit: capture recovered when the streams are available again. If the server was started while the display was already asleep, run mac-vnc-server wakeup and start it again.
If testing a generic client, try:
./.build/release/mac-vnc-server --encoding zrle
./.build/release/mac-vnc-server --encoding zlib
./.build/release/mac-vnc-server --encoding rawUse another port:
./.build/release/mac-vnc-server --port 5903
open 'vnc://127.0.0.1:5903'Run:
./.build/release/mac-vnc-server permissions
./.build/release/mac-vnc-server diagnoseThen restart the server after granting permissions.