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.
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:
| Provider | Backs | Ships in | macOS |
|---|---|---|---|
SystemModelProvider | Apple's on-device model | Core | 26+ |
PCCModelProvider | Apple Private Cloud Compute — needs Apple's managed entitlement, in your app's main executable (SDK guide, §10e) | Core | 27 |
ClaudeModelProvider | Claude, through the Foundation Models interface | LocalLMLabSDKClaude | 27 |
MLXModelProvider | Open-weight models you download and run locally | LocalLMLabSDKInference | 27 |
RemoteModelProvider | Hosted providers over an OpenAI-compatible API — GPT, Claude's online API, OpenRouter, or any compatible server, with provider-native web search | LocalLMLabSDKRemote | 27* |
*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:
- Check before you fetch.
validate(_:)runs a preflight (repo reachable, MLX format, an architecture the runtime supports, size against memory, disk space) and names the stage that failed. A host-suppliedMLXModelTrustPolicydecides which repos may be fetched at all — the SDK ships no allow-list, that's your call. - Verify what you fetched. Every downloaded file is hashed against the hash Hugging
Face reports for it before it's kept (on by default), and an
MLXCacheLimitscap bounds the whole cache. - Pin every model to one exact commit.
pinnedRevisions:is the version you shipped;pinStore:captures the version a user first downloaded;managedPinStore:records a developer-vouched move of a shipped pin. - Update and clean up deliberately.
checkPinUpdatepreviews what would change;updatePin(_:to:beforeSwitch:)downloads and verifies, waits for your pause point, moves the pin, and evicts — all or nothing, so a running conversation is never left half on the old version and half on the new.snapshots(for:)andremoveSnapshotlet you list and safely remove old versions; the SDK never prunes on its own. - Tune and pair.
SessionOptionscarries temperature, top-p / top-k / min-p, a seed, a maximum output length, a repetition penalty and more into the model, and now reaches Apple's on-device model too.pairDraftModeladds a smaller speed helper (speculative decoding) andpairAdaptera LoRA adapter, each switchable per run. - Measure every turn, and don't let one run away. New in 2.0.
Each turn reports the model's real token counts, prompt and generation speed (pp / tg) and MLX
memory — resident, peak, and the GPU's limit — through
MLXUsageMetadataKey.MLXPrefillMonitorsays what the model is reading while nothing has appeared yet. A prompt too long for the context window or the GPU memory left is refused before it runs (and compacted and retried if you've setretryOnContextOverflow), and a turn that keeps calling tools stops aftermaxModelCallsPerTurnmodel calls — 16 unless you set your own.
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:
- Selection.
Sequence<any Tool>.limited(toMaxImpact:)caps which tools even reach a session — hand the model a read-only slice, or exclude delete tools outright. For an MCP server, each tool starts off until you turn it on. - Approval, per MCP server (new in 2.0). The server is the unit of trust: you decide how far to believe it, and what its tools may do without asking — answered by the user, or by your own policy function (an IT rule set, a policy service, a rule over the arguments). The settings are below.
- One authorizer.
ConfirmingToolAuthorizerapplies it on every call — the model's and an interactive view's alike — wired to whatever confirmation UI you use. It fails closed: no one to ask, no policy, or no answer in time means no.
| Level | Setting | Options | What it decides |
|---|---|---|---|
| Server | TrustsetTrust(_: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. |
| Server | ApprovalsetToolApproval(_: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. |
| Tool | On / OffsetToolEnabled |
On · Off (new tools start off) | Whether the model is offered the tool at all. |
| Tool | Approval | 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.
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).
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.
↑ TopStart 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.
| Example | Start 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.
↑ TopLicensed 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.
↑ TopComing 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:
- Tool authorization.
RuleBasedToolAuthorizerandConfirmingToolAuthorizer'srequirement:closure are gone. Say who may approve what where the tools come from — per MCP server onlab.mcp(setToolApproval) and for your own tools on the authorizer (hostTools:) — and move anything finer-grained into apolicy:function. A 1.xrequirementmaps mechanically:.allow→.allow,.confirm→.askUser,.deny→.deny. See Security. - Annotation trust.
setTrustsToolAnnotations(true, server:)is nowsetTrust(.trusted, server:). A trusted server now asks only before deletions by default (1.x asked before every change); set its approval to keep the old behaviour. - One MCP manager. An app that uses
LocalLMLabuseslab.mcp— configured throughLocalLMLab.Configuration(mcp:)— rather than making its ownMCPServerManager. - Behaviour. A session's MCP tools now follow
lab.mcpbetween turns (a tool you turn on reaches the model on its next turn). To keep a session on a fixed set of MCP tools, pass them intools:withincludeMCPTools: false. Tools an MCP server marks as only for its interactive view are no longer offered to models. - Running a turn. Use
session.respond(to:)/streamResponse(to:); callinglanguageModelSessiondirectly still works but skips the chat history,turnContextand the tool refresh. - Local (MLX) models. A turn that keeps calling tools now stops after 16 model
calls with an error saying so; raise it with
MLXModelProvider(maxModelCallsPerTurn:)or per session (Int.maxfor no limit). A prompt that can't fit is refused before it runs, asLocalLMLabError.context.usagenow reports real token counts (input was always 0) —languageModelSession.usagesums the whole session, so read one turn's from its response.contextBudgetnow measures the latest turn rather than the whole session. - New enum cases.
MCPProtocolVersionandMCPConnectionModegained a case each; an exhaustiveswitchneeds@unknown default.
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:
MCPServerManager.callToolnow returns anMCPToolResultinstead of aString; useresult.renderedForModelfor the old string. (Going throughMCPToolorFileBackedToolneeds no change.)MCPConnectionStatusandMCPServerErrorare non-frozen, so an exhaustiveswitchneeds@unknown default.- A custom
ModelProviderconformance changes:languageModel(for:)→makeSession(for:…).
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.
Get it
The SDK, the reference apps, and the full developer guide are in the
LocalLM GitHub repo
(the 2.3.0-GA branch):
- The SDK developer guide (also on GitHub as docs/sdk-guide.md) — the full developer guide: linking Core, the model layer (§6a), entitlements, the MCP client (§3), connectors, App Sandbox / Mac App Store signing, ready-made vs. hand-written tool-calling (§7a), and a full API reference.
- docs/migrating-to-2.0.md — upgrading from 1.0.
- docs/migrating-to-1.0.md — upgrading from the 0.8.x line.
- docs/tested-models.md — which open-weight (MLX) models actually tool-call.
- MCPAppsHost — showing MCP servers' interactive views (MCP Apps), open source.
- docs/mcp-diagnostics.md — the MCP client's logging.
- docs/api-surface.md · CHANGELOG.md · examples/ · Components
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