An MCP client for your app
The Model Context Protocol client from LocalLM Lab, as a Swift package — no protocol or OAuth stack to write
LocalLM Lab is built on a Swift SDK, and its MCP client ships in
that SDK's LocalLMLabSDKCore. Link it into your own macOS app and
MCPServerManager gives you tool discovery, all the auth flows, Keychain-backed
token storage, and the current spec revision — wired, by default, straight to Apple's
on-device model. This page is the MCP-client piece; the
SDK overview covers the rest (connectors, workspace access, drop-in
SwiftUI).
Why local AI + MCP together is the point
Every MCP client in wide use today assumes a cloud model on the other end of the tool
call — Claude, GPT, Gemini — with the whole tool-calling loop running on someone else's
servers. This client is wired to Apple's on-device FoundationModels model
by default: the model choosing the tool, the request going out to a real MCP server,
and the result coming back all happen with the model running locally on the Mac's own
Neural Engine. Nothing about that — the prompt, the tool selection, the reasoning —
has to leave the device to reach a model provider just so your app can be AI-powered.
That's a different privacy and security posture than any cloud-model MCP client can
offer, and no per-token bill for the reasoning step itself.
The same client keeps working the same way if your app also offers a hosted model. Route a session to Claude's online API, GPT, or OpenRouter instead, and its tool calls still go through this MCP client, 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 the server. A cloud model never talks to an MCP server directly here, whichever model is doing the reasoning.
What "a real MCP client" means here
MCP ships a dated revision every few months, and "supports MCP" can mean anything from the
2024 tool-calling core to the current spec. This client speaks
2026-07-28, the current revision (new in
2.0), and negotiates downward automatically: a server still on
2025-11-25, or as old as 2025-03-26, connects and works. You
don't pick a version, and you don't touch the wire — addServer(url:) does
discovery or the initialize handshake, sessions where a server uses them, the
per-request headers, and multi-round-trip input requests for you. Servers that send
change notifications keep their tool list current by themselves;
MCPServerState.liveUpdates tells you when that's active.
What that buys a developer who is not an MCP expert:
- All three auth models, auto-detected. Start every connection with
authType: .none. If the server needs OAuth, the client catches the 401, reads the challenge, runs the full OAuth 2.1 + PKCE browser flow, and stores the token in the Keychain (scoped to your bundle ID). If the server supports Dynamic Client Registration it registers on the fly; if not, you get a distinct error that's your cue to ask for a client ID. New in this revision: Client ID Metadata Documents — setMCPOAuthFlow.clientMetadataURLto a static JSON file describing your app and skip per-server registration entirely. - Structured tool results. A tool call returns an
MCPToolResult, not a bare string: text,structuredContent(JSON the server marked as data, validated against the tool's declared output schema),resourceLinks(pointers the model can ask to follow), anisErrorflag, and a truncation flag.renderedForModelfolds all of it into one context-sized string when that's all you need. - Server-initiated requests. Newer servers can pause a tool call to
ask the user for input (elicitation), ask your model to run a prompt
(sampling), or ask which files they may see (roots). Each is an
optional handler you register — nothing is advertised to a server unless you opt in.
Componentsships the elicitation UI (below); the rest are seams you fill only if you need them. - Untrusted by construction. Every server response is size-capped
(
MCPResponseLimits), newly discovered tools and resources default to disabled, and a tool description is treated as attacker-controlled text. A server is untrusted until you or the user say otherwise — its own read-only / destructive labels are ignored, so every call to it asks first. Mark a server you vouch for as trusted and choose, per server, what runs without asking (new in 2.0; SDK security). - Diagnostics for when a user reports a problem.
MCPDiagnostics— always-onos.Loggerlines plus an opt-in in-memory event buffer you dump as text — covers the connection, auth, and stream sequence. Strictly off-content: never prompt or response text, tokens redacted.
Drop-in SwiftUI, not just an API
Components ships ready-to-drop-in SwiftUI: an "add an MCP server" screen
supporting all three auth types, tool and resource enable/disable controls, an
OAuth-waiting view for the browser round trip, and a one-click summary export of what a
connected server offers. You don't have to build MCP management UI from scratch.
It also ships the elicitation sheet: when a server pauses a tool call to
ask the user for input, MCPElicitationPresenter +
.mcpElicitationSheet(_:) show a typed, validated form that names the server
asking and returns Submit / Decline / Cancel — three lines to wire, nothing to design.
See the elicitation UI for what it renders; conform to
MCPElicitationHandler yourself if you'd rather own the presentation.
A server's own views (MCP Apps) — new in 2.0
Some servers ship an interactive view with a tool — Todoist's task list, a system
monitor's live graphs. The open-source LocalLMLabSDKMCPAppsHost package shows
that view in your app's conversation when the model calls the tool, in a sandboxed web view,
and routes the view's own tool calls through the same per-server approval as the model's.
Declare support once with
MCPSettings(handlers: MCPClientHandlers().advertisingMCPApps()) — servers
only offer views to a client that says it can show them.
MCP Apps in your app →
Test it against a running server
The LocalLM Lab app is this same client with a UI on it — which makes it a way to check an MCP server you're building against an independent implementation and a range of models, with a wire-level protocol log. Testing your MCP server →
Or skip the app and build your own test tool straight on the SDK.
MCPServerManager's addServer / toolsForSession /
callTool are plain async methods with no model involved —
callTool(server:tool:arguments:) takes the arguments you supply and
hands back the result, so you can assert against a schema or an expected value without a
model ever deciding what to call. That's a small Swift CLI you run by hand during
development, a suite of scripted assertions for QA — or, on a macOS runner with
Xcode 27, a headless check in CI/CD: no GUI, no Apple Intelligence requirement, no model
download, fully deterministic. (Whether a model reliably picks and calls your tool is a
different question, and needs a real model in the loop — that's what the app itself, or
repo-qa below, are for.)
Start from working code
| 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. Builds a Tool from a live server's own schema. |
plate-today-tools |
an OAuth-gated MCP server (Todoist) alongside the connectors in a signed SwiftUI
app, using Core's ready-made Tools. |
components-demo |
to drop in the prebuilt SwiftUI MCP-server management UI instead of wiring one connection yourself. |
mcp-chat |
a local-model chat that shows a server's interactive views (MCP Apps) in the conversation, with per-server trust and approval. |
Docs
- The SDK developer guide
— §3 is the MCP client: the three auth types + CIMD, protocol revisions up to
2026-07-28and what each adds (§3a), structured results, elicitation and diagnostics (§3b–§3e), Keychain storage, ready-made vs. hand-written tool-calling (§7a). - docs/mcp-diagnostics.md — the client's logging: the verbosity knob, reading Apple's unified log, the exportable buffer for a user bug report.
- examples/ · Components · the repo
Contact
Building on this, or hit a rough edge? neuron@thisbrain.ai or our Discord.
↑ Top