The MCP server runs in the same binary as the GUI, but it runs in a different process. When an AI assistant like Claude Code spawns AutoShelf in MCP mode, it gets a fresh process. That process needs to talk to the already-running GUI app that has all the folder watchers, the rule store, and the sandbox bookmarks active.
This is the core architectural challenge: how do two processes, both running the same sandboxed binary, communicate with each other?
Why Not Just Use Distributed Objects or XPC?
macOS has several built-in IPC mechanisms. XPC is the most modern and is well supported in sandboxed apps. But XPC is designed for services that are packaged inside the app bundle. It assumes the service is a separate binary that the app launches itself.
In my case, the MCP process is launched by an external AI assistant. The app did not spawn it. So XPC does not really fit.
Distributed Objects (NSConnection) is deprecated and flaky with sandbox restrictions.
I landed on a POSIX Unix domain socket. It is simple, it works in a sandbox (with the right entitlements), and it does not require any Apple-specific framework. The downside is that I had to write all the serialization and dispatch logic myself.
The Socket Setup
The socket lives in the app's group container. On a real Mac App Store install, the path is:
~/Library/Group Containers/group.com.autoshelf.mac/ipc.sock
During development without the group container set up, it falls back to:
/tmp/com.autoshelf.mac.ipc/ipc.sock
The server code in SocketServer.swift creates the socket, binds to it, and starts listening. Here is the rough flow:
- Create the directory if it does not exist
- Remove any stale socket file from a previous run
- Call
socket(AF_UNIX, SOCK_STREAM, 0)to get a file descriptor - Call
bind()with the socket address - Call
chmod(777)so the MCP process can connect - Call
listen(5)to start accepting connections - Loop on
accept()in a background queue
Each connection reads a JSON-encoded IPCRequest, dispatches it to the main actor for processing, and sends back a JSON-encoded IPCResponse.
Why chmod 777?
This one tripped me up for a while. Sandboxed processes run as the same user but with different sandbox containers. The socket file is owned by the first process that creates it. Without world permissions, the second process cannot connect.
chmod 777 is not ideal from a security standpoint, but the socket is only accessible to the same user on the same machine. It lives inside a directory that is only readable by that user. The risk is low.
The IPC Protocol
I defined a simple JSON protocol with 16 commands. Each request has a command field and optional parameters:
struct IPCRequest: Codable, Sendable {
let command: IPCCommand
let ruleName: String?
let filePath: String?
let jsonOutput: Bool?
let ruleData: IPCRuleData?
let folderType: String?
let isFromMCP: Bool?
}
The isFromMCP flag is important. It tells the server that this request came from an MCP client, which means write operations need an extra permission check. More on that later.
The response is equally simple:
struct IPCResponse: Codable, Sendable {
let success: Bool
let message: String?
let rules: [IPCRuleInfo]?
let monitoringActive: Bool?
let proLicensed: Bool?
let ruleData: IPCRuleData?
let groupNames: [String]?
let mcpEnabled: Bool?
let mcpWriteEnabled: Bool?
}
Every command gets a response. Even if the command fails, the response has success: false and a human-readable message.
The Auto-Launch Dance
When the CLI tool or MCP bridge starts, it tries to connect to the socket. If the app is already running, the connection succeeds immediately. If the app is not running, the connection fails.
In that case, the client launches the app using open -b com.autoshelf.mac and retries in a loop. Here is the retry logic from SocketClient.swift:
- Try to connect with a 1-second timeout
- If it fails, run
open -n -g -b com.autoshelf.mac - Wait 500ms and retry
- Repeat up to 16 times (8 seconds total)
This works because open -b is synchronous-ish. The app starts, initializes its socket server, and becomes ready within a second or two. The retry loop catches it on the second or third attempt most of the time.
The -g flag launches the app in the background. No dock icon, no window. The user does not see anything happen. The app is now running and ready to accept commands.
Handling the Folder Picker Remotely
One of the trickier parts of the IPC design was the folder picker. When an AI assistant wants to create a rule, it needs to specify a watched folder. But the MCP server runs in a headless process. It cannot show a native macOS folder picker dialog.
The solution is that the pick_folder IPC command tells the GUI app to pop an NSOpenPanel. The GUI app handles the modal dialog, and sends the selected folder path back through the socket.
This means the IPC connection has to stay open while the user clicks around Finder and selects a folder. I set the timeout on these requests to 300 seconds (5 minutes). Most users pick a folder in 10-20 seconds, but some take their time browsing.
What I Learned About Sandbox IPC
The sandbox does not block Unix domain sockets between processes that share the same app group. Apple explicitly allows this. The key entitlement is com.apple.security.application-groups, which I set to ["group.com.autoshelf.mac"].
Both the GUI process and the MCP process need this entitlement. Since they are the same binary with the same embedded entitlements, this works automatically.
The other sandbox constraint I had to work around is that sandboxed processes cannot use arbitrary network sockets. Unix domain sockets are fine because they are filesystem-based. But if I had tried to use TCP or UDP sockets for IPC, the sandbox would have blocked them.
The Final Architecture
Here is how the pieces fit together:
- The CLI tool (a separate Go binary) connects to the socket directly
- The MCP bridge (the app in MCP mode) connects to the socket via SocketClient
- The GUI app owns the socket server and all the real state (rules, watchers, bookmarks)
Everything flows through the same socket. The same permission checks, the same rule validation, the same error handling. The GUI app is the source of truth, and the MCP and CLI are just remote control surfaces.
This design has held up well through two major releases. I have not had a single IPC-related crash or deadlock since shipping it.