SDK

Build a native macOS AI app on the same foundation LocalLM Lab runs on

LocalLM Lab itself is built on a Swift SDK, LocalLMLabSDKCore, and you can build on the same thing. It gives your app one or more models it can actually call tools with, a real MCP client, native access to the user's calendar / reminders / contacts / location / files, and drop-in SwiftUI for the parts users configure — without an EventKit wrapper, an OAuth stack, or an MCP implementation to write yourself.

Want to jump straight into the deep end? The full SDK developer guide covers everything on this page in detail, with code.

2.3.0 is now generally available. LocalLMLabSDKCore keeps a macOS 26 deployment floor — one build serves macOS 26 and 27, with the hosted providers, Private Cloud Compute, and open-weight (MLX) models registered behind #available(macOS 27, *). Consuming the SDK needs Xcode 27 (the xcframeworks are built with the macOS 27 SDK). 2.0 is a one-time breaking release (coming from 1.x); every 2.x release is source compatible with 2.0.0 — additive changes only until 3.0.

Why build on it

Build for privacy and local control — and that holds even when you reach for a hosted model. Apple's on-device model and open-weight MLX models never leave the device: no cloud calls, no per-token bill for the reasoning step, a genuinely different security posture than a cloud-model app. But the control doesn't stop at reasoning. Route a session to a hosted provider instead — Claude's online API, GPT, OpenRouter — and its tool calls still run through the SDK's own MCP client and connectors, on the user's Mac, with the user's own OAuth tokens from their own Keychain. The hosted model sends back a tool call; your app is what actually reaches Slack or GitHub. It never talks to those services directly, whichever model is doing the reasoning.

That's on top of the usual reasons to build on a shared SDK rather than your own stack: one MCP client instead of a protocol and OAuth implementation to write and maintain, ready-made connector Tools instead of an EventKit wrapper, and a model layer that owns routing, residency, and download state so your app doesn't reinvent a provider abstraction. It's not a demo dependency — LocalLM Lab itself runs on this SDK.

↑ Top

What's in it

A model layer, a real MCP client, system connectors and scoped filesystem access, a security/authorization layer for what a model is allowed to do unattended, drop-in SwiftUI for the parts users configure — and, new in 2.0, what a chat app needs to show a conversation, including an MCP server's own interactive views.

The model layer — new in 1.0

Offer more than one model without writing your own provider abstraction. Five built-in providers sit behind one ModelProvider protocol — on-device, Apple's cloud, a hosted provider, or an open-weight model you run yourself, all reached the same way:

ProviderBacksShips inmacOS
SystemModelProviderApple's on-device modelCore26+
PCCModelProviderApple Private Cloud Compute — needs Apple's managed entitlement, in your app's main executable (SDK guide, §10e)Core27
ClaudeModelProviderClaude, through the Foundation Models interfaceLocalLMLabSDKClaude27
MLXModelProviderOpen-weight models you download and run locallyLocalLMLabSDKInference27
RemoteModelProviderHosted providers over an OpenAI-compatible API — GPT, Claude's online API, OpenRouter, or any compatible server, with provider-native web searchLocalLMLabSDKRemote27*

*LocalLMLabSDKRemote itself has a macOS 26 manifest floor — it links into a 26-deployment app fine — but RemoteModelProvider is 27-only, same gate as the other three. Name models with routes (.heavy, .light, …), pick one per session, keep one warm between turns, and surface download and memory state in your UI. On macOS 26 the 27-only providers simply aren't registered; lab.models.availability(for:) reports them as .requiresOS("macOS 27") and ModelPickerView shows them as disabled rows.

In practice that's a small adoption diff, not a rewrite. This is the entire change repo-qa-local makes over the Apple-model repo-qa it's built from — register an MLXModelProvider, route a model ID to it, validate/download once if it isn't local yet, then make the session the same way you would for any provider:

let mlx = MLXModelProvider(
    residentModelLimit: 1,
    pinnedRevisions: ["mlx-community/Qwen3-8B-4bit": "545dc4251c05440727734bcd94334791f6ab0192"],  // the commit you reviewed
    pinStore: MLXFilePinStore())                        // a user's own choice is pinned on first download
let lab = LocalLMLab(configuration: .init(providers: [mlx, SystemModelProvider()]))
let modelID = ModelID(scheme: "mlx", rest: "mlx-community/Qwen3-8B-4bit")!
lab.models.route(.local, to: modelID)

if case .notDownloaded = lab.models.availability(for: modelID) {
    _ = try await mlx.validate(modelID.rest)                // fits this Mac's RAM? MLX format?
    for try await event in mlx.download(modelID.rest) { … }  // stream progress %
}

let session = try lab.makeSession(route: .local, tools: tools, instructions: instructions)
for try await partial in session.streamResponse(to: task) { … }   // the reply so far

makeSession merges your tools with the enabled MCP tools whatever the route — your own Tools, the connectors, and the MCP client attach to on-device, Claude, and MLX sessions alike. MLXModelProvider also owns the memory story: validate preflights a model with no download, capabilityProbe is the authoritative check of whether a downloaded model can actually tool-call, and residentModelLimit caps how many stay in RAM. For a starting shortlist of which open-weight models tool-call, see tested-models.md.

A real MCP client

MCPServerManager connects your app to Model Context Protocol servers — Slack, GitHub, Notion, Linear, and the rest — at the current spec revision (2026-07-28, new in 2.0, negotiating down for older servers). All three auth models auto-detected, OAuth 2.1 + PKCE + DCR + CIMD, Keychain-backed tokens, structured tool results, server-initiated elicitation, live tool-list updates, and off-content diagnostics. The same tool-calling loop runs on top of whichever provider you route to — Apple on-device, Claude, or a downloaded open-weight model. The client tracks the spec as it moves. The MCP client →

Running downloaded models safely, tuned and paired

An app that downloads models at runtime inherits a real risk: a Hugging Face repo isn't a fixed, vetted artifact. Its owner can change the files behind a name, anyone can publish a look-alike, and a download can be corrupted or too big for the Mac. MLXModelProvider covers that end to end, and you opt into as much of it as your app needs:

mlx-control-room shows all of it working, with a gauge beside each knob; vistanova shows the minimal end (one pinnedRevisions:). The walkthrough is sdk-guide.md, and the app's own tuning and pairing controls are described on the AI Models page.

Reading a turn's token counts, speed and memory, and setting the tool-call limit, are covered in Measuring and guarding a local model's turn in sdk-guide.md. LocalLM Lab's Benchmark panel is built on the same API — see it for what the numbers look like on a real run.

Mac system connectors

Calendar, Reminders, Contacts, Location, Clock, Weather, and a scoped Filesystem — the data and Apple features already on the user's Mac, each with a ready-made Tool so most apps don't hand-write a tool-calling adapter, and the real macOS permission prompts handled for you. Each ready-made Tool carries an impact rating (read, mutate, delete), so you decide which ones reach a session (see Security below). Every connector stays on-device except Weather, which is a real call out to Apple's own weather service — the one connector that isn't purely local. What each connector does →

Scoped filesystem access

WorkspaceAccess / WorkspaceTools give the model read / write / edit / search / patch over one folder the user grants — the security-scoped-bookmark plumbing an App Sandbox app needs, done.

Built for a model that gets things wrong, or is following instructions planted in what it reads (hardened in 2.0). Every file is opened from the granted folder without following symbolic links, so a link pointing elsewhere on the Mac — even one swapped in mid-operation — is refused. Writes land whole or not at all, a new file never overwrites one that appeared in the meantime, and delete removes single files, never folders. Reads, searches and edits are bounded (WorkspaceLimits), so a huge log in the folder can't stall the app, and the whole API is async and cancellable.

FileBackedTool and the "AIQL" data verbs go further (a saved result lands in a raw/ subfolder, data-file types only, so a server's output can't overwrite your source or config): they let the model pull data out of a source — an MCP server's dataset, a local file — and analyze or process it entirely on the user's Mac, without the raw rows ever entering the model's context. The mechanism is natural language → SQL: the model writes one read-only query against the data, your app runs it, and only the query's shape (or a resulting CSV) comes back — so a model can answer questions over rows it never actually saw, and can't fabricate or miscopy a value it was never shown. See aiql for the full pipeline.

Security & tool authorization

Every tool Core ships carries an impact rating (read, mutate, delete) baked in, so you can control what a model is allowed to do unattended without hand-rolling your own classification. Two levers, independent of each other — which tools a session can see, and whether the ones it can see ask before running:

LevelSettingOptionsWhat it decides
ServerTrust
setTrust(_:server:)
Untrusted (default) · Trusted Whether the server's own labels (read-only, destructive) are believed. Untrusted: every tool counts as one that may change data.
ServerApproval
setToolApproval(_:server:)
Allow — nothing asks
Ask before changes — reads run
Ask before deletions
Ask before every call
Default — untrusted asks before changes, trusted before deletions
Which calls need authorization before they run.
Server↳ Who answers an ask The user (a confirmation sheet) · Your policy (allow, deny, or ask the user) Who authorizes the calls that need it.
ToolOn / Off
setToolEnabled
On · Off (new tools start off) Whether the model is offered the tool at all.
ToolApproval Coming in a later release A tool's own allow / ask, overriding its server's — e.g. allow a search tool on an untrusted server.

Settings are read on every call, so a change takes effect on the next one — no new session. To stop being asked about deletions on a server you rely on, set it to Allow, or send them to your policy so a rule decides and no one is interrupted. Walkthrough: SDK guide §7c.

RuleBasedToolAuthorizer and the requirement: closure from 1.x are gone in 2.0 — one way to say who may approve what. security-demo is the runnable reference: a frontier model against Calendar and a Todoist MCP server, with nothing but a Security panel and a Run button.

Drop-in SwiftUI (Components)

An "add an MCP server" screen with all three auth types, tool / resource toggles, each server's trust and approval, an OAuth-waiting view, the elicitation sheet, and a tool-call confirmation sheet keyed off each tool's impact rating — the ConfirmingToolAuthorizer UI, ready-made. For models you download, there's a validate → download → pin onboarding stepper, an update view (check, update, roll back) and a versions-on-disk view with a guarded Remove. Optional, layered strictly on Core's public API.

Chat apps and MCP Apps — new in 2.0

Run each turn with session.respond(to:) or streamResponse(to:) and draw session.hostTranscript: the user's message as soon as it's sent, each tool call as a live record with its full result, the reply as it streams, and the model's reasoning split from its answer — never shortened when the model's own context is compacted. turnContext sends the date and time with every message (a model has no clock), MCP tools the user turns on reach the model on its next turn, and a conversation can be saved and reopened with the model's memory of it.

When an MCP server ships an interactive view with a tool — an MCP App, such as Todoist's task list — the open-source LocalLMLabSDKMCPAppsHost package shows it in the conversation when the model calls that tool, sandboxed, with the view's own tool calls going through the same authorizer as the model's. MCP Apps in your app →

Decision models (Jev) — new in 2.0

A decision model doesn't write text: it answers a few fixed questions about an input — yes / no, one of several options, a point on a scale — with a probability for each answer, in milliseconds. lab.decide asks the same questions of TypeSafe's Jev (through OpenRouter), Featherless's Simple Jev, OpenAI's Decisions API (new in 2.3; it can also decline a question, in Decision.refusals), or OpenJev, the SDK's own decider on a local MLX model, chosen by route like a chat model. lab.models.pair(decision:generator:) keeps a small decider and your chat model loaded together (or shares one model between them), and the open-source JevDK playground takes questions from first draft to a tested, calibrated file your app loads. Decision models in your app →

Two paths to tool-calling

Drop in a ready-made Tool for each connector and for any MCP server tool (built at runtime from its own schema) — or write your own adapter against the same underlying calls for full control over names, schemas, and descriptions. Both ship in Core; mixing them is fine.

↑ Top

Proven under App Sandbox, not assumed

Sandboxing an app breaks all sorts of things silently — network calls, background permissions, subprocess spawning. Rather than assume the SDK would just work under sandbox, it was built into a sandboxed test app, hit real breakage (a missing network entitlement that silently killed weather lookups and MCP connections), fixed, and re-verified: Calendar, Reminders, and the full MCP+OAuth+Keychain flow all confirmed working under sandbox, with real system permission prompts. There's also a working path to a Mac App Store submission: an Apple Distribution signing + provisioning pipeline that produces a correctly signed .pkg with a signature chain verified up to Apple's own root CA. A downloaded open-weight model runs inside the sandbox too, fetching its weights into the app's own container (workspace-buddy-local).

↑ Top

Dogfooded in a real, shipping app

LocalLM Lab doesn't keep a private copy of this code around — it depends on the SDK directly. When this page says Calendar or MCP access "works," it means it works in a real app real users run, not just in a sample project.

↑ Top

Start from working code

Open-source reference apps ship with the SDK — use the closest one as a starting reference rather than writing from scratch. Each has a README with copy-paste setup.

ExampleStart here if you want…
repo-qa to connect to an MCP server you don't control — the smallest example, a CLI, no signing or permissions.
plate-today / -tools connectors (Calendar, Reminders) plus an OAuth-gated MCP server (Todoist) in a signed SwiftUI app — the same app on the hand-written and ready-made tool-calling paths, to diff.
workspace-buddy to give the model scoped access to a folder of files under App Sandbox — WorkspaceTools plus the security-scoped-bookmark pattern.
components-demo to drop in the prebuilt SwiftUI MCP-server management UI instead of wiring one connection yourself.
repo-qa-local the smallest "download and run an open-weight model" — repo-qa with an MLX model instead of Apple's, about 30 lines different, including the pin.
code-buddy the full model-layer pattern: a CLI coding agent with .heavy / .light routes to two local models, Workspace + host Process tools, an MCP docs server, and a persistent >> session.
workspace-buddy-local a sandboxed GUI app running a downloaded model — the Mac App Store shape, model download working inside the App Sandbox container.
model-switch hosted providers — GPT / Claude's online API / OpenRouter and on-device behind one chat call site, with provider-run web search and citations.
mcp-chat a chat app with a local model where an MCP server's own interactive views (MCP Apps) show up in the conversation — Todoist's task list for “what's due today?” — with streaming, per-server trust and approval, and conversations that reopen.
jevdk to design and test decision-model questions: run them on a local MLX model and on hosted Jev side by side, score a batch against your marked answers, calibrate, compare models, and export the tested questions for your app (decision models, JevDK guide).
security-demo to see the security/authorization layer in isolation — a frontier model against Calendar and a Todoist MCP server, gated by nothing but a Security panel.
aiql plain-English → one read-only SQL SELECT → CSV over an MCP dataset; rows never enter the model's context, so nothing is fabricated.
vistanova a small search engine on local models and an MCP server (Tavily), with the MLX summary model pinned to an exact commit so an upstream change never silently changes what your app runs.
mlx-control-room every MLX knob with a gauge that proves it changed something, plus the supply-chain flow made visible: validate, download, pin, update, roll back, clean up, and speed-helper / adapter pairing.
components-updates-demo the Components onboarding, update and versions-on-disk views, driven by simulated sources so every state (a denied preflight, a failed update, a rollback) is reachable.
os-matrix one .macOS("26.0") build that runs on both macOS 26 and 27, with the 27-only model families gated by ModelAvailability.requiresOS — no source #if.

annotated-examples.md walks every one line by line, SDK touchpoints marked inline.

↑ Top

Licensed to actually be used commercially

Apache 2.0 — chosen specifically for its patent grant, which matters if your legal team has to sign off on embedding a third-party SDK into a commercial product. Versioned binary releases are published on GitHub with checksums.

↑ Top

Coming from 1.x?

Most code compiles unchanged: 1.x's leftover overloads (makeSession, respond, the PendingToolCall and LocalLMLab.Configuration initializers) are folded into single APIs whose usual call sites still build. What you edit:

The full list, with what to write instead, is under Breaking in the CHANGELOG; step by step in migrating-to-2.0.md.

Coming from 0.8.x?

Mostly additive — the MCP client, connectors, and Keychain storage carry over, and the deployment floor stays at platforms: [.macOS("26.0")]. The source-breaks:

Coming from a 1.0 beta? The changes since beta.4 that can break a build (tool-call SessionEvent payloads, InstalledModel's initializer, a required cancelDownload(_:), a throwing RemoteModelProvider initializer) are listed under Changed — breaking (pre-GA) in the CHANGELOG. Full detail in migrating-to-1.0.md.

↑ Top

Get it

The SDK, the reference apps, and the full developer guide are in the LocalLM GitHub repo (the 2.3.0-GA branch):

Point knownSDKReleases at 2.3.0-GA with the checksum from the release's .sha256 asset. Each example pins an explicit SDK version:

LOCALLM_SDK_VERSION=2.3.0-GA swift build
↑ Top

Contact

Questions, or building something with this? neuron@thisbrain.ai or our Discord.

↑ Top