WRFRMS, our Mac wireframing app still in development, is built around frames, devices, a palette of UI primitives, and files that serialize to JSON. When we started thinking about exposing it to agents, a few properties made it an unusually clean fit for MCP. The domain is small and well-typed, with about thirty element kinds and a dozen device presets. Every user action is a pure mutation on a document, and the document is already a single file on disk. That meant we could expose the app’s own verbs like add_element, move_element, resize_element, and clear_frame as MCP tools without inventing a new abstraction layer. An agent that can read the document and call those tools has exactly the same leverage over a wireframe as a human with a mouse.
Keeping the Server Close to the App
We implemented the server in TypeScript on top of @modelcontextprotocol/sdk. The tools are thin wrappers over a small core loop: readProject, then mutate, then writeProject, with atomic renames and an mtime check that rejects writes if the app saved underneath us.
The important detail was not the transport. It was behavior. We ported the app’s own layout logic, including the snap-into-device rules that anchor nav bars to the top, footers to the bottom, and heroes full-width, directly into the server in src/element-factory.ts. That way an element placed by MCP lands exactly where the UI would have placed it. On the Swift side, DocumentViewModel file-watches the open project and hot-reloads when the server writes, briefly suppressing the watcher around its own atomic writes so save-reload-save loops do not thrash.
The Mac File System Is the Hard Part
A few Mac-specific sharp edges showed up quickly in practice. NSSavePanel appends .json because we declared allowedContentTypes = [.json], so a file the user types as mcp.wrfrms lands on disk as mcp.wrfrms.json. The app’s default save directory is ~/Documents/WRFRMS rather than wherever the user’s project source happens to live.
Figuring out which file the agent is actually editing mattered more than we expected. In theory MCP is about tools and structured calls. In practice, a large part of reliability comes down to whether the path on disk is the path the human thinks is open in the app.
Shipping Instructions with the Server
The single highest-leverage addition was passing an AGENT_INSTRUCTIONS string to the server’s instructions init field. Every MCP client forwards that to the agent on connect, so Claude Code, Cursor, or anything else gets a condensed operator brief immediately: tool workflow, coordinate conventions, default element sizes, snap behavior, and known limitations.
That removed a surprising amount of friction. Before that change, users would burn a large part of a session teaching the model that x and y are element centers rather than top-left coordinates, that add_element for type=deviceFrame hardcodes deviceKind=iPhone, or that the file on disk might not be the file the app currently has open. Once those details shipped as part of the server itself, the agent started informed instead of spending the first half of a session reconstructing invariants from trial and error.
Lessons for Other Mac Apps
If you are building MCP into your own Mac app, expose the verbs your app already has instead of inventing new ones. Agents reason better about “add a button at (x, y)” than about an abstract CRUD schema disconnected from the product’s actual language.
Port any non-trivial domain logic like layout, validation, or snapping into the server instead of approximating it. Identical behavior on both sides is what makes hot-reload feel seamless instead of uncanny.
Ship instructions from day one, and put the non-obvious parts there: file-location quirks, sharp edges, and limitations you have not fixed yet. Budget specifically for the Mac file-system corner cases too, especially NSSavePanel extension munging, ~/Documents versus project-folder confusion, and the coordination dance between file-watching and your own atomic writes.
An mtime check plus atomic rename plus a short watcher-suppression window is maybe fifty lines of code, and it saves you from an entire class of “the app saved over my edit” bugs. Users will not thank you for the work they never see, but they will absolutely notice when their wireframe silently disappears.