Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
c4d2a8f
repo-qa-local: use lab.mcp, let makeSession build the MCP tools
ancientcomputing Oct 3, 2026
2a418d8
aiql: data tools from lab.mcp, file-backed; build against 2.0.0-dev
ancientcomputing Oct 3, 2026
abe22e8
Merge remote-tracking branch 'origin/1.0.0-GA' into 1.1.0-dev
ancientcomputing Oct 3, 2026
ffe60e9
Merge branch '1.1.0-dev' into 2.0.0-dev (1.0.0-GA release work)
ancientcomputing Oct 3, 2026
6bf6480
docs: 2.0 migration guide and CHANGELOG entry (skeleton for the MCP p…
ancientcomputing Oct 3, 2026
46f4373
docs: MCP 2026-07-28 client support in the SDK guide, migration guide…
ancientcomputing Oct 3, 2026
4ca921d
docs: chat apps and MCP Apps in the SDK guide (2.0)
ancientcomputing Oct 3, 2026
890b6ce
aiql (synced): run the turn with session.respond, not languageModelSe…
ancientcomputing Oct 3, 2026
1f7cc96
docs: MRTR lines now log under their own MCP.mrtr category
ancientcomputing Oct 3, 2026
4402faf
docs: SDK guide — per-server trust and approval, one authorizer (2.0)
ancientcomputing Oct 3, 2026
cd63819
security-demo (synced) + walkthrough: 2.0 per-server approval
ancientcomputing Oct 3, 2026
419288d
docs: MRTR asks the user once per Cancel/Decline
ancientcomputing Oct 3, 2026
3485def
Components (synced): MCPServerPickerView shows server trust and appro…
ancientcomputing Oct 3, 2026
879fdd0
repo-qa-local (synced): run the turn with session.respond, not langua…
ancientcomputing Oct 3, 2026
8d5b7d7
Cascade 2.0.0-dev SDK checksums
ancientcomputing Oct 3, 2026
568f5c4
MCPAppsHost (2.0): host-side MCP Apps support, shipped as source
ancientcomputing Oct 3, 2026
e7e2e6d
mcp-chat (2.0): MCP Apps in a chat with a local model
ancientcomputing Oct 3, 2026
6b0a1a4
docs: annotate examples/mcp-chat
ancientcomputing Oct 3, 2026
21253aa
docs: api-surface from the published v2.0.0-dev binaries; lab.mcp not…
ancientcomputing Oct 3, 2026
bd2d7a5
docs: bring the MCP examples' annotations up to date with their 2.0 s…
ancientcomputing Oct 3, 2026
a718be0
examples: run turns with session.respond / streamResponse (2.0)
ancientcomputing Oct 3, 2026
7b639c3
docs: explain respond/streamResponse vs the languageModelSession esca…
ancientcomputing Oct 3, 2026
a8e47d1
Cascade republished 2.0.0-dev SDK checksums (Core, Inference, Remote,…
ancientcomputing Oct 3, 2026
dd67385
examples: add Xcode project for mcp-chat
ancientcomputing Oct 3, 2026
832a143
2.0 changelog and migration guide: local (MLX) model changes
ancientcomputing Oct 4, 2026
93d5fc3
flush-sdk-cache: clear SwiftPM's artifact cache entries (they're files)
ancientcomputing Oct 4, 2026
bc06354
Cascade republished 2.0.0-dev SDK checksums (Core, Inference, Remote,…
ancientcomputing Oct 4, 2026
06aa9e7
examples: add JevDK, the decision API (Jev) playground
ancientcomputing Oct 4, 2026
860ec1b
examples: add Xcode project for jevdk
ancientcomputing Oct 4, 2026
65a9fca
jevdk: no key file; keys from the Keychain or the environment
ancientcomputing Oct 4, 2026
290c758
examples: jevdk release script for a signed, notarized DMG
ancientcomputing Oct 4, 2026
6b4968d
docs: decision models (Jev) in the SDK guide, migration guide, annota…
ancientcomputing Oct 4, 2026
0c4751d
JevDK: hosted results show A, B, C labels like local ones
ancientcomputing Oct 4, 2026
cd97a99
JevDK: Dock icon
ancientcomputing Oct 4, 2026
28eeb9e
JevDK icon: a small "ev" under the J
ancientcomputing Oct 4, 2026
53bc6d1
JevDK: a File menu that says which file is which
ancientcomputing Oct 4, 2026
65db74d
jevdk: README links the web version of the guide
ancientcomputing Oct 4, 2026
fd801f0
JevDK: "Export Questions…" (was "Export Questions for App…")
ancientcomputing Oct 4, 2026
fa739da
JevDK: About panel with the icon, version and a link to the decision-…
ancientcomputing Oct 4, 2026
b272fdf
jevdk release script: duplicate keychain entries of one certificate a…
ancientcomputing Oct 4, 2026
829a402
2.0 docs: decision models (Jev) in the CHANGELOG, README, SDK guide s…
ancientcomputing Oct 4, 2026
0e6d8e1
sdk-guide: measuring and guarding a local model's turn (2.0)
ancientcomputing Oct 4, 2026
40ad53c
JevDK: drop internal shorthand and the old-name Keychain migration
ancientcomputing Oct 4, 2026
5d6a5d1
annotated-examples: jevdk excerpts and counts brought up to date
ancientcomputing Oct 4, 2026
78b06c1
CHANGELOG (2.0): MCP resource sent verbatim; number x-mcp-header reje…
ancientcomputing Oct 4, 2026
a7cbae1
2.0 docs: hardened, async WorkspaceAccess and the saveAs policy
ancientcomputing Oct 4, 2026
a7600c9
Docs: calibrate in JevDK, use the calibration in your app
ancientcomputing Oct 5, 2026
05968a9
Docs: where the three calibration numbers come from (X, Y, Z)
ancientcomputing Oct 5, 2026
e35e4b5
Docs: say where the calibration screenshot comes from
ancientcomputing Oct 5, 2026
64772ff
Docs: calibration in your app, explained in plain English first
ancientcomputing Oct 5, 2026
ab21a79
Docs: decisions that need your product's knowledge
ancientcomputing Oct 5, 2026
5168057
docs: JevWire in the CHANGELOG and the SDK guide's reference
ancientcomputing Oct 5, 2026
4a4c9bc
JevDK: Export Server Config, and model versions shown (jev-serve S0b)
ancientcomputing Oct 5, 2026
c78f7db
examples: add jev-serve, hosted Jev's API answered by OpenJev on this…
ancientcomputing Oct 5, 2026
1dfc823
Cascade republished 2.0.0-dev SDK checksums (Core, Inference, Remote,…
ancientcomputing Oct 5, 2026
b8f7cbe
jev-serve release zip, --version; JevDK release script finds NOTICE i…
ancientcomputing Oct 5, 2026
0ef3d4c
JevDK carries jev-serve; JevDK → Install jev-serve Command…
ancientcomputing Oct 5, 2026
baa919e
annotated-examples: jev-serve added; jevdk brought up to date
ancientcomputing Oct 5, 2026
330198f
README: JevDK download from ancientcomputing/jevdk; jev-serve comes w…
ancientcomputing Oct 5, 2026
7393597
sync-jevdk-repo: keep the jevdk repo's own SDK version (SDK_VERSION=……
ancientcomputing Oct 5, 2026
4701352
JevDK: the app icon in the bundle (AppIcon.icns), so Finder and the D…
ancientcomputing Oct 5, 2026
3462e72
Cascade 2.0.0-GA SDK checksums
ancientcomputing Oct 7, 2026
75cf09e
Docs: 2.0.0-GA download links and version
ancientcomputing Oct 7, 2026
cdfe98c
Docs: api-surface from the 2.0.0-GA binaries; tested-models GA note
ancientcomputing Oct 7, 2026
0464d8f
CHANGELOG: 2.0.0 heading carries its GA date (2026-10-07)
ancientcomputing Oct 8, 2026
6886d17
CHANGELOG 2.0.0: Anthropic streaming crash fix; checksums
ancientcomputing Oct 8, 2026
936d2e5
Cascade re-published 2.0.0-GA SDK checksums
ancientcomputing Oct 9, 2026
3dc067c
CHANGELOG 2.0.0: loadTable recordsAt fallback
ancientcomputing Oct 9, 2026
4500dd1
aiql README: Qwen3.8-27B-4bit as the option for a larger Mac
ancientcomputing Oct 9, 2026
a149e3c
Cascade 2.0.0-GA SDK checksums (re-published 2026-10-09, loadTable fix)
ancientcomputing Oct 9, 2026
2a6d9cd
components-updates-demo: SDK 2.0.0-GA in the README; come to the fron…
ancientcomputing Oct 9, 2026
f7597a5
model-switch README: how to write each provider's model id
ancientcomputing Oct 9, 2026
2bf2e9d
sdk-guide: the workspace tools are Core's, and the app owns the folde…
ancientcomputing Oct 10, 2026
886939c
localai-cli READMEs: where the model comes from; current download loc…
ancientcomputing Oct 11, 2026
a53148f
annotated-examples: break up the opening paragraph
ancientcomputing Oct 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 225 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Components/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ struct SDKRelease {
// or edit `defaultSDKVersion`. For a release not listed, add its entry (URL follows the pattern
// below; checksum is the `.sha256` next to the zip on that GitHub release). Keep this in step
// with `examples/model-switch/Package.swift`, which consumes Core through this package.
let defaultSDKVersion = "1.0.0-GA"
let defaultSDKVersion = "2.0.0-GA"

let knownSDKReleases: [String: SDKRelease] = [
"1.0.0-RC.1": SDKRelease(
url: "https://github.com/ancientcomputing/locallm/releases/download/v1.0.0-RC.1/LocalLMLabSDKCore-1.0.0-RC.1.xcframework.zip",
checksum: "397e7b5f7efd1076293a3d5d06c41d75043bffa23cffdb821d71d21ee41e68de"
),
"1.0.0-GA": SDKRelease(
url: "https://github.com/ancientcomputing/locallm/releases/download/v1.0.0-GA/LocalLMLabSDKCore-1.0.0-GA.xcframework.zip",
checksum: "7d77a9c2e37dfb2f7925f01ed011262ee5a2aebc561a3ae7a93524acc0285b3e"
),
"2.0.0-GA": SDKRelease(
url: "https://github.com/ancientcomputing/locallm/releases/download/v2.0.0-GA/LocalLMLabSDKCore-2.0.0-GA.xcframework.zip",
checksum: "5f9418a6879b22227afdf867cf474d426b93ae36d783d5a958efaae1e347681b"
),
]

func failManifest(_ message: String) -> Never {
Expand Down
106 changes: 104 additions & 2 deletions Components/Sources/LocalLMLabSDKComponents/MCPServerPickerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,35 @@ public struct MCPServerPickerView: View {
.frame(width: 140)
}

// Radio group, not a segmented control. A segmented `Picker` with 3 long labels
// flips between "hug content" and "fill the frame" distribution across layout passes
// (an empty vs. filled `TextField` reports a different ideal width, and any keystroke
// re-runs layout), so it visibly resized and its label vanished as you typed in the URL
// field. A radio group has no width ambiguity and shows all three options at once.
// Same control as LocalLM Lab's MCP Servers view.
Picker("Auth type", selection: $newServerAuthType) {
Text("None").tag(MCPAuthType.none)
Text("Personal Access Token").tag(MCPAuthType.pat)
Text("OAuth (manual client)").tag(MCPAuthType.oauthManual)
}
.pickerStyle(.segmented)
.frame(width: 420)
.pickerStyle(.radioGroup)
Text("A server that uses OAuth with dynamic client registration (most hosted servers) needs None: you sign in in your browser when you click Add.")
.font(.system(size: 12))
.foregroundStyle(.secondary)

if newServerAuthType == .pat {
SecureField("Personal access token", text: $newServerPATToken)
.textFieldStyle(.roundedBorder)
Text("The token's own permissions are what the server gets — this app doesn't narrow them.")
.font(.system(size: 12))
.foregroundStyle(.secondary)
}
if newServerAuthType == .oauthManual {
TextField("Client ID (from the server's developer console)", text: $newServerManualClientID)
.textFieldStyle(.roundedBorder)
Text("For servers with no dynamic client registration (e.g. Slack). Create an app in the server's developer console, opt into PKCE if offered, register \(MCPOAuthFlow.redirectURI) as a redirect URL, then paste the Client ID here — no client secret needed. You'll still sign in in your browser when you click Add.")
.font(.system(size: 12))
.foregroundStyle(.secondary)
}

Button(adding ? "Adding…" : "Add") {
Expand Down Expand Up @@ -136,6 +150,8 @@ public struct MCPServerPickerView: View {
.font(.system(size: 12, design: .monospaced))
.foregroundStyle(.secondary)

trustControls(server)

if !server.tools.isEmpty {
Text("\(server.tools.filter(\.enabled).count) of \(server.tools.count) tools enabled — ~\(server.estimatedTokens) tokens")
.font(.system(size: 12))
Expand Down Expand Up @@ -194,6 +210,92 @@ public struct MCPServerPickerView: View {
}
}

// MARK: trust and approval

/// The approvals offered here. An approval decided by the app's policy (`by: .policy`) is the
/// app's setting, shown but not offered.
private enum ApprovalChoice: Hashable {
case automatic, allow, askChanges, askDeletions, askEverything, appPolicy

init(_ approval: ToolApproval?) {
switch approval {
case nil: self = .automatic
case .allow?: self = .allow
case .ask(_, .policy)?: self = .appPolicy
case .ask(.read, .user)?: self = .askEverything
case .ask(.mutate, .user)?: self = .askChanges
case .ask(.destructive, .user)?: self = .askDeletions
case .ask?: self = .askChanges
@unknown default: self = .askChanges
}
}

var approval: ToolApproval? {
switch self {
case .automatic: nil
case .allow: .allow
case .askChanges: .ask(atOrAbove: .mutate, by: .user)
case .askDeletions: .ask(atOrAbove: .destructive, by: .user)
case .askEverything: .ask(atOrAbove: .read, by: .user)
case .appPolicy: nil
}
}
}

@ViewBuilder
private func trustControls(_ server: MCPServerState) -> some View {
let id = server.id
HStack(spacing: 16) {
Picker("Trust", selection: Binding(
get: { server.trust },
set: { manager.core.setTrust($0, server: id) }
)) {
Text("Untrusted").tag(MCPServerTrust.untrusted)
Text("Trusted").tag(MCPServerTrust.trusted)
}
.fixedSize()
Picker("Before a tool runs", selection: Binding(
get: { ApprovalChoice(server.toolApproval) },
set: { manager.core.setToolApproval($0.approval, server: id) }
)) {
Text(server.trust == .trusted ? "Default: ask before deletions" : "Default: ask before changes")
.tag(ApprovalChoice.automatic)
Text("Don't ask").tag(ApprovalChoice.allow)
Text("Ask before changes").tag(ApprovalChoice.askChanges)
Text("Ask before deletions").tag(ApprovalChoice.askDeletions)
Text("Ask before every call").tag(ApprovalChoice.askEverything)
if ApprovalChoice(server.toolApproval) == .appPolicy {
Text("Decided by the app's policy").tag(ApprovalChoice.appPolicy)
}
}
.fixedSize()
.disabled(ApprovalChoice(server.toolApproval) == .appPolicy)
}
.controlSize(.small)
Text(server.trust == .trusted
? "Trusted: the server's own labels decide which tools only read and which delete. Trust only operators you vouch for — a server can label anything."
: "Untrusted: the server's labels are ignored, so every tool counts as one that may change data.")
.font(.system(size: 11))
.foregroundStyle(.secondary)

if server.toolsChangedSinceTrusted {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle").foregroundStyle(.orange)
Text("This server's tools changed since you trusted it. Review them below.")
Button("Trust Again") { manager.core.setTrust(.trusted, server: id) }
}
.font(.system(size: 12))
}
if server.connectionStatus == .connected, !server.tools.isEmpty,
!server.tools.contains(where: { $0.enabled && $0.isModelVisible }) {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.circle").foregroundStyle(.orange)
Text("None of this server's tools are on, so a model can't use it yet. Turn on the ones you want below.")
}
.font(.system(size: 12))
}
}

private func saveSummaryToFile(_ server: MCPServerState) {
let panel = NSSavePanel()
panel.nameFieldStringValue = "\(server.displayName)-mcp-tools.txt"
Expand Down
38 changes: 38 additions & 0 deletions MCPAppsHost/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// swift-tools-version: 6.0
import PackageDescription

// LocalLMLabSDKMCPAppsHost — host-side support for MCP Apps (SEP-1865, spec 2026-01-26): the
// `ui://` widgets an MCP server ships and a host renders in a sandboxed view. Open source
// (Apache 2.0), shipped as SOURCE like Components, and built on LocalLMLabSDKCore's PUBLIC API
// only.
//
// Core comes from Components, which declares the LocalLMLabSDKCore binary target and re-vends it
// (two packages declaring that binary target is a hard SwiftPM error). So this package has no
// SDK version or checksum of its own: it builds against whatever Components' `defaultSDKVersion`
// / LOCALLM_SDK_VERSION selects. Requires SDK 2.0.0-GA or later.
//
// What is here: metadata parsing (`_meta.ui`) and the `ui://` resource loader (mime check, size,
// SHA-256); `MCPAppsBridge`, the JSON-RPC state machine between a widget and the host, with an
// explicit policy and an audit sink; `MCPAppViewController` / `MCPAppView`, the sandboxed
// WKWebView; `MCPAppsSessionBackend`, which routes a widget's calls through a LocalLMLabSession;
// and the widget lifecycle — `MCPAppWidgetCache`, `MCPAppViewPool`, `MCPAppRecreation`.
let package = Package(
name: "LocalLMLabSDKMCPAppsHost",
platforms: [.macOS("26.0")],
products: [
.library(name: "LocalLMLabSDKMCPAppsHost", targets: ["LocalLMLabSDKMCPAppsHost"])
],
dependencies: [
.package(path: "../Components")
],
targets: [
.target(
name: "LocalLMLabSDKMCPAppsHost",
dependencies: [.product(name: "LocalLMLabSDKCore", package: "Components")]
),
.testTarget(
name: "LocalLMLabSDKMCPAppsHostTests",
dependencies: ["LocalLMLabSDKMCPAppsHost"]
)
]
)
160 changes: 160 additions & 0 deletions MCPAppsHost/Sources/LocalLMLabSDKMCPAppsHost/MCPAppMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import CryptoKit
import Foundation
import LocalLMLabSDKCore

// `_meta.ui` parsing for MCP Apps (SEP-1865). Everything here reads the verbatim `_meta`
// `MCPValue` Core surfaces on tool descriptors and resource contents; nothing is trusted — a
// malformed or hostile `_meta` degrades to "no UI" / "no permissions", never to a wider grant.

// Tool-side MCP Apps parsing (`MCPToolDescriptor.app`, `.appVisibility`, `.isModelVisible`,
// `MCPAppVisibility`, `MCPAppLink`) lives in LocalLMLabSDKCore, so every host applies the same
// visibility rules. This file keeps the resource side: what a fetched `ui://` widget declares.

/// The CSP origins a `ui://` resource asks for (`_meta.ui.csp`).
public struct MCPAppCSP: Sendable, Equatable {
public var connectDomains: [String] = []
public var resourceDomains: [String] = []
public var frameDomains: [String] = []
public var baseUriDomains: [String] = []

public init(connectDomains: [String] = [], resourceDomains: [String] = [], frameDomains: [String] = [], baseUriDomains: [String] = []) {
self.connectDomains = connectDomains
self.resourceDomains = resourceDomains
self.frameDomains = frameDomains
self.baseUriDomains = baseUriDomains
}

/// True when the resource asks for no origins at all (Todoist's widget declares exactly this).
public var isEmpty: Bool {
connectDomains.isEmpty && resourceDomains.isEmpty && frameDomains.isEmpty && baseUriDomains.isEmpty
}
}

/// Device permissions a `ui://` resource asks for (`_meta.ui.permissions`).
public struct MCPAppPermissions: Sendable, Equatable {
public var camera = false
public var microphone = false
public var geolocation = false
public var clipboardWrite = false

public init(camera: Bool = false, microphone: Bool = false, geolocation: Bool = false, clipboardWrite: Bool = false) {
self.camera = camera
self.microphone = microphone
self.geolocation = geolocation
self.clipboardWrite = clipboardWrite
}

public var isEmpty: Bool { !(camera || microphone || geolocation || clipboardWrite) }
}

/// What a `ui://` resource declares about itself in `_meta.ui`. These are a server's *requests*;
/// the host decides what to grant (intersect with policy, never widen).
public struct MCPAppResourceInfo: Sendable, Equatable {
public var csp: MCPAppCSP
public var permissions: MCPAppPermissions
public var domain: String?
public var prefersBorder: Bool?

public init(csp: MCPAppCSP = MCPAppCSP(), permissions: MCPAppPermissions = MCPAppPermissions(), domain: String? = nil, prefersBorder: Bool? = nil) {
self.csp = csp
self.permissions = permissions
self.domain = domain
self.prefersBorder = prefersBorder
}

/// Parses a resource's `_meta`. `nil` or malformed input yields the empty (most restrictive)
/// declaration.
public init(meta: MCPValue?) {
self.init()
guard case .object(let top)? = meta, case .object(let ui)? = top["ui"] else { return }
func strings(_ value: MCPValue?) -> [String] {
guard case .array(let items)? = value else { return [] }
return items.compactMap { if case .string(let s) = $0 { return s } else { return nil } }
}
if case .object(let csp)? = ui["csp"] {
self.csp = MCPAppCSP(
connectDomains: strings(csp["connectDomains"]),
resourceDomains: strings(csp["resourceDomains"]),
frameDomains: strings(csp["frameDomains"]),
baseUriDomains: strings(csp["baseUriDomains"]))
}
if case .object(let perms)? = ui["permissions"] {
func has(_ key: String) -> Bool { perms[key] != nil && perms[key] != .bool(false) && perms[key] != .null }
permissions = MCPAppPermissions(
camera: has("camera"), microphone: has("microphone"),
geolocation: has("geolocation"), clipboardWrite: has("clipboardWrite"))
}
if case .string(let d)? = ui["domain"] { domain = d }
if case .bool(let b)? = ui["prefersBorder"] { prefersBorder = b }
}
}

public enum MCPAppResourceError: Error, Equatable, CustomStringConvertible {
case notAUIResource(String)
case wrongMimeType(String?)
case noBody
case invalidBase64

public var description: String {
switch self {
case .notAUIResource(let uri): return "\(uri) is not a ui:// resource"
case .wrongMimeType(let m): return "expected text/html;profile=mcp-app, got \(m ?? "no mimeType")"
case .noBody: return "resource has neither text nor blob"
case .invalidBase64: return "resource blob is not valid base64"
}
}
}

/// A fetched `ui://` widget: its HTML, what it declared, and the SHA-256 of the exact bytes — the
/// value an IT allowlist pins.
public struct MCPAppResource: Sendable, Equatable {
public static let mimeType = "text/html;profile=mcp-app"

public let uri: String
public let html: String
public let info: MCPAppResourceInfo
/// Lowercase hex SHA-256 of the UTF-8 HTML.
public let sha256: String

/// Validates and wraps a `resources/read` result. `listedMeta` is the `_meta` from
/// `resources/list`; the content entry's own `_meta` wins when both exist (spec: the read
/// result is authoritative).
public init(content: MCPResourceContent, requestedURI: String, listedMeta: MCPValue? = nil) throws {
guard requestedURI.hasPrefix("ui://") else { throw MCPAppResourceError.notAUIResource(requestedURI) }
let normalizedMime = content.mimeType?.lowercased().filter { !$0.isWhitespace }
guard normalizedMime == Self.mimeType else { throw MCPAppResourceError.wrongMimeType(content.mimeType) }
let html: String
if let text = content.text {
html = text
} else if let blob = content.blob {
guard let data = Data(base64Encoded: blob), let decoded = String(data: data, encoding: .utf8) else {
throw MCPAppResourceError.invalidBase64
}
html = decoded
} else {
throw MCPAppResourceError.noBody
}
self.uri = requestedURI // never the server-echoed URI
self.html = html
self.info = MCPAppResourceInfo(meta: content.meta ?? listedMeta)
self.sha256 = SHA256.hash(data: Data(html.utf8)).map { String(format: "%02x", $0) }.joined()
}
}

extension MCPClientHandlers {
/// The client capability key under which a host declares MCP Apps support at `initialize`.
public static let mcpAppsExtensionKey = "io.modelcontextprotocol/ui"

/// These handlers, declaring MCP Apps support (`text/html;profile=mcp-app` widgets). Servers
/// that gate their widgets on it (`_meta.ui` on tools) only offer them to a client that says so.
///
/// ```swift
/// let lab = LocalLMLab(configuration: .init(
/// providers: [...], mcp: MCPSettings(handlers: MCPClientHandlers().advertisingMCPApps())))
/// ```
public func advertisingMCPApps() -> MCPClientHandlers {
var handlers = self
handlers.extensions[Self.mcpAppsExtensionKey] = .object(["mimeTypes": .array([.string(MCPAppResource.mimeType)])])
return handlers
}
}
Loading
Loading