Check out our partner site, https://dreamofelectric.com
← Back to Blog

Ship the Claude extension inside your app, not in a config file

by
engineering mcp claude macos get-bananas

Ship the Claude extension inside your app, not in a config file

When we shipped Get Bananas, the studio's handwritten shopping list, the Mac app came with something I haven't seen many native apps do yet: a Claude Desktop extension built directly into the app bundle. Open Settings on the Mac, click Install Extension, accept the prompt Claude Desktop shows you, and from then on Claude can read and edit your real shopping list in conversation. Ask "what's on my list?" and it tells you. Ask "add eggs and milk" and they appear on your iPhone the next time iCloud sweeps through.

There is no API key. There is no signup. There is no server of ours in the loop. And critically, there is no step where you copy a JSON snippet into a config file buried in ~/Library.

That last part is the whole point of this post.

The setup tax most MCP servers charge

The Model Context Protocol is a genuinely good idea: a standard way to give an LLM tools that read and write real state. But the default distribution story for an MCP server is rough. You install a Node package or clone a repo, you find your Claude Desktop config file, you hand-edit JSON to register the server, you restart the client, and you hope you got the path right. That flow is fine for developers wiring up their own tools. It is a non-starter for a shipping consumer app.

If Get Bananas required its users — people who downloaded a shopping list app — to edit a JSON config file to enable the Claude integration, approximately none of them would ever do it. The feature would exist and not be used. That's the same as not shipping it.

The primitive Apple already handed us

Here's what changed my thinking. Claude Desktop registers extensions from a .mcpb bundle — a single file that means "register me with Claude." macOS already has a verb for "open this file with the app that handles it": NSWorkspace.shared.open().

So the entire install flow in Get Bananas is one call, on a file shipped inside the app bundle:

// The .mcpb is bundled with the app and ships when the app ships.
if let url = Bundle.main.url(forResource: "get-bananas", withExtension: "mcpb") {
    NSWorkspace.shared.open(url)
}

That's it. The user taps Install Extension, macOS hands the bundled .mcpb to Claude Desktop, and Claude Desktop shows its own confirmation prompt. The user accepts inside Claude, not inside a text editor. No path to get wrong, no JSON to malform, no restart instructions to follow.

The extension lives inside the app bundle, ships when the app ships, and updates when the app updates. There is no separate registry to publish to, no per-user setup, no "paste this URL into your config." If you want Claude to be able to do something inside your app, you put the something inside the app and let the OS introduce them.

Keep the tool surface small

The MCP server inside Get Bananas exposes exactly five tools: read the list, add an item, remove one, update one, replace the whole thing. There are no clever convenience helpers, no read-modify-write wrappers, no batch operations. The surface follows what a person would actually ask for in conversation — "what's on my list," "add milk," "take off the eggs we already bought" — and stops there.

This is a discipline, not a limitation. Every tool you expose to a model is a tool the model can call wrong, a behavior you have to document, and a line in the contract between your app and the LLM. The smaller the surface, the more predictable the integration, and the easier it is for a user to reason about what they just authorized.

One JSON file is the whole backend

The thing that makes this safe is that the MCP server isn't talking to a server of ours. It reads and writes a single file in the user's own iCloud Drive container — BananaList.json, in the Get Bananas container. The iOS, iPad, Mac, and Watch apps read and write that same file. The MCP server reads and writes that same file. iCloud handles sync. We handle nothing.

The concurrency model between Claude and the running app fits in one acquire/release function: a lock file (.lock) sits next to the JSON, writes go to a process-tagged temp file and atomically rename onto the real path, and a five-second stale-lock timeout self-cleans if something dies mid-write. That's the entire coordination layer. There's no database, no daemon, no message queue — because the only two writers are the app and the extension, and they're both writing one small file in a folder Apple already syncs.

We wrote about the full architecture in the Get Bananas product overview, and the founder case study on blakecrosley.com goes deeper on the per-locale data work. The MCP extension is open source, so anyone curious can read the entire data path: it's the JSON file, and that's all it ever touches.

The studio's stance on AI in apps

This connects to a pattern that runs through everything the studio ships. Captain's Log runs its AI journal summaries on-device via Apple Foundation Models first, with optional bring-your-own-key OpenAI and Anthropic as a fallback, and never proxies anything through a studio server. Ki, our private browser, does the same with its on-device assistant. Get Bananas does it with the Claude Desktop extension: you already authenticated with Claude Desktop, so there's no key for us to hold, no proxy for us to run, and nothing for us to leak.

The principle: on-device first, bring-your-own-key second, never proxied through a studio service. Anything else trades the user's ownership of their own data for a roadmap. Making the AI integration work without a server of ours in the middle isn't a constraint we worked around — it's the design.

Why this matters for other builders

I think the "extension inside the app bundle" pattern is going to matter for a lot of native apps over the next year. The MCP ecosystem is converging on the idea that apps should expose tools to assistants. The distribution problem — how a normal user actually enables that — is the part still being figured out. Shipping the extension inside the app bundle and registering it with one OS-level call is the cleanest answer I've found: no registry, no config file, no setup tax. The app already exists on the user's machine. The capability ships with it.

If you're building a native app and want Claude to be able to act inside it, don't make the user wire it up. Put the .mcpb in your bundle, hand it to the OS, and let the platform make the introduction.

— Blake