
引子
话说京城近日不太平,东厂的番子们总想着搞些 "天外飞仙" 式的新奇暗器,咱们的大内密探零零发 ------ 这位放着琴操姑娘的柔情不管、专爱捣鼓发明的主儿,哪能坐视不管?
这不,他把 "要你命三千" 的零件往旁边一扔,一头扎进了 iOS 开发的 "密探工具箱",誓要做个能查发明状态、能跟军师聊天的神器。
在本篇京城八卦中,您将学到如下内容:
- 引子
- 🛠️ 第一回:立规矩!MCP 服务器的 "武功心法"
- 📡 第二回:造 "发明检测器"!查状态全靠它
- 🗣️ 第三回:找个 "军师"!Anthropic API 当参谋
- 📱 第四回:搭 "聊天棚"!MCP 客户端的门面
- 🎬 尾声:神器初成,就差最后一步!
毕竟对付坏人,光靠蛮力可不够,还得懂点 Swift 代码,这才叫 "文武双全" 嘛!:)

🛠️ 第一回:立规矩!MCP 服务器的 "武功心法"
零零发常说:"做发明跟练武功一个理,得先有招式框架,不然就是花拳绣腿。" 要做这密探神器,第一步就得给 "MCP 服务器" 立个规矩 ------ 也就是定义个协议,规定它得会两样本事:一是有能用的 "工具列表",二是能 "调用工具干活"。
咱们先看看这 "武功心法" 的代码模样,零零发在草稿纸上是这么写的:
swift
import Foundation
// 定义MCP服务器的核心协议(相当于武功秘籍的总纲)
// 任何想当MCP服务器的"发明",都得遵守这规矩
protocol MCPServerProtocol {
// 必须有一套能用的工具(比如查发明、测暗器的工具)
var tools: [Tool] { get }
// 必须能调用工具,还得是异步的(毕竟查发明得等一会儿)
func call(_ tool: Tool) async throws -> String
}
// 工具结构体(相当于武功里的"招式细节")
// 得告诉别人这工具叫啥、能干啥、要啥参数
struct Tool: Encodable {
// 编码用的Key,跟服务器约定好叫啥名
enum CodingKeys: String, CodingKey {
case name, toolDescription = "description", input_schema
}
let name: String // 工具名(比如"查最新发明")
let toolDescription: String // 工具功能说明
let input_schema: [String: String] // 输入参数格式(必须有,哪怕不用)
}
写完后,零零发一拍桌子说到:"瞧见没?这协议就像'大内密探行为准则',你想当服务器,就得按这来!不然你那工具连名字都没有,跟东厂番子没带腰牌一样 ------ 谁认得你啊?"

📡 第二回:造 "发明检测器"!查状态全靠它
规矩立好了,接下来就得造个真能用的 "服务器"------ 零零发给它起名叫LingLingFaInventionService
,专门查最新的密探发明状态,比如 "天外飞仙破解器" 的电量、校准情况。
毕竟这破解器要是没电了,遇到无相皇可就麻烦了!

咱们先看看这 "发明检测器" 的代码,零零发还贴心加了注释:
swift
// 密探发明服务(真正的MCP服务器实现,相当于"要你命三千"的核心部件)
final class LingLingFaInventionService: MCPServerProtocol {
// 给服务器装工具:这里就一个------查最新发明状态
var tools: [Tool] = [
Tool(
name: "check_latest_invention", // 工具名:查最新发明
toolDescription: "获取密探最新发明状态(如天外飞仙破解器的电量、校准情况)from 密探发明库。", // 功能说明
input_schema: ["type": "object"] // 输入参数格式(这里不用参数,但按规矩得写)
)
]
// 懒加载"密探发明库"(相当于存发明数据的仓库,类似HealthKit)
private lazy var inventionStore = LingLingFaInventionStore()
// 调用工具的核心方法:别人叫工具,就走这儿
func call(_ tool: Tool) async throws -> String {
// 先判断是不是咱们认识的工具,不是就扔错(总不能让它查东厂的暗器吧)
guard tool.name == "check_latest_invention" else {
throw InventionError.toolNotSupported // 自定义错误:工具不支持
}
// 调用方法查最新发明状态,返回结果
let inventionStatus = try await fetchLatestInventionStatus()
return inventionStatus
}
// 真正查发明状态的方法(核心逻辑,相当于给"破解器"读数据)
private func fetchLatestInventionStatus() async throws -> String {
// 第一步:请求"密探发明库"的授权(不然没权限查数据,跟进宫要腰牌一样)
try await inventionStore.requestAuthorization(toShare: [], read: [.latestInvention])
// 第二步:查最新的发明数据(按条件查,只找最新的那条)
let descriptor = InventionQueryDescriptor(
predicates: [.sample(type: .latestInvention)], // 条件:最新发明
sortDescriptors: [] // 排序:不用排,直接要最新的
)
// 从发明库拿数据
let samples = try await descriptor.result(for: inventionStore)
// 第三步:判断有没有数据,没有就扔错(总不能查个空气吧)
guard let sample = samples.first as? InventionSample else {
throw InventionError.missingInventionData // 自定义错误:没找到发明数据
}
// 第四步:提取数据(比如电量、校准状态),组装成字符串返回
let deviceName = sample.deviceName // 发明名:天外飞仙破解器
let battery = sample.batteryPercentage // 电量:80%
let isCalibrated = sample.isCalibrated // 校准状态:已校准
return "\(deviceName):电量\(battery)%,状态:\(isCalibrated ? "已校准" : "未校准")"
}
}
// 自定义的"密探发明库"(模拟用,相当于HealthKit的替代品)
class LingLingFaInventionStore {
// 请求授权的方法(模拟)
func requestAuthorization(toShare: [InventionType], read: [InventionType]) async throws {
// 实际开发里这里要弹授权框,现在先模拟"授权通过"
print("密探发明库授权通过!零零发:'搞定!'")
}
}
// 模拟的查询描述符(相当于查数据的"命令符")
struct InventionQueryDescriptor {
let predicates: [InventionPredicate]
let sortDescriptors: [NSSortDescriptor]
// 模拟拿数据的方法
func result(for store: LingLingFaInventionStore) async throws -> [Any] {
// 模拟返回一条数据:天外飞仙破解器,电量80%,已校准
return [InventionSample(
deviceName: "天外飞仙破解器",
batteryPercentage: 80,
isCalibrated: true
)]
}
}
// 模拟的发明数据模型
struct InventionSample {
let deviceName: String
let batteryPercentage: Int
let isCalibrated: Bool
}
// 自定义错误(清晰明了,不然报错都不知道哪儿错了)
enum InventionError: Error {
case toolNotSupported // 工具不支持
case missingInventionData // 没找到发明数据
}
// 模拟的发明类型和条件(凑齐逻辑用)
enum InventionType { case latestInvention }
struct InventionPredicate { static func sample(type: InventionType) -> Self { Self() } }
零零发还补了句小贴士:"哎我说,想在模拟器测这功能?你得先打开'密探工具箱'APP,在'发明库'里加条测试数据 ------ 就像我给'要你命三千'装刀片一样,没零件它咋干活?

对了,真要上真机,还得在 Info.plist 里加'密探发明库使用说明',不然系统不让你用,跟东厂查腰牌一样严!"
🗣️ 第三回:找个 "军师"!Anthropic API 当参谋
光能查发明还不够,零零发想让神器能 "聊天"------ 比如问一句 "我那天外飞仙破解器弄得咋样了?",就有个 "军师" 帮着分析,还能自动调用工具查。
他找的 "军师" 就是 Anthropic 的 Claude LLM,用 API 连上去,相当于给神器装了个 "脑子"。

这 "军师接口" 的代码,零零发是这么写的:
swift
import Foundation
// 军师服务(对接Anthropic API,相当于给神器找个参谋)
final class ClaudeAdvisorService {
private let apiKey: String // 军师的"通关文牒"(API Key,得从Anthropic官网拿)
private let tools: [Tool] // 告诉军师咱们有啥工具(比如查发明的工具)
// 初始化:得给通关文牒和工具列表
init(apiKey: String, tools: [Tool]) {
self.apiKey = apiKey
self.tools = tools
}
// 给军师发消息的方法(核心:把密探的话传给军师,等回复)
func send(messages: [Request.Message]) async throws -> Response {
// 1. 确定军师的地址(API URL)
guard let url = URL(string: "https://api.anthropic.com/v1/messages") else {
throw AdvisorError.invalidURL // 错误:地址不对
}
var request = URLRequest(url: url)
// 2. 装消息头:告诉军师是谁、用啥版本的规矩
request.httpMethod = "POST" // 用POST方法发消息(跟递奏折一样)
request.setValue(apiKey, forHTTPHeaderField: "x-api-key") // 通关文牒
request.setValue("2023-06-01", forHTTPHeaderField: "anthropic-version") // 规矩版本
// 3. 装消息体:告诉军师用啥模型、说啥话、有啥工具
let body = Request(
model: "claude-3-opus-20240229", // 军师的"脑子型号"(Claude 3 Opus)
messages: messages, // 聊天记录(之前说的话)
max_tokens: 1024, // 军师最多说多少话(防止说起来没完)
tools: tools // 咱们有的工具,告诉军师
)
// 4. 把消息体编码成JSON(相当于把奏折写成文言文,军师才懂)
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase // 键名转蛇形(符合API要求)
request.httpBody = try encoder.encode(body)
// 5. 发消息给军师,等回复(异步等,不然卡着不动)
let (data, _) = try await URLSession.shared.data(for: request)
// 6. 把军师的回复解码成咱们能懂的格式(文言文转白话文)
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase // 键名转驼峰
return try decoder.decode(Response.self, from: data)
}
}
// 发给军师的请求格式(相当于奏折的格式)
struct Request: Encodable {
let model: String // 军师型号
let messages: [Message] // 聊天记录
let max_tokens: Int // 最大回复长度
let tools: [Tool]? // 可用工具
// 单条消息的格式(谁发的、说啥)
struct Message: Encodable {
// 角色:密探(user)还是军师(assistant)
enum Role: String, Encodable {
case user = "user" // 密探(用户)
case assistant = "assistant" // 军师(助手)
}
let role: Role // 角色
let content: [Content] // 消息内容(可能是话,也可能是工具调用)
}
}
// 军师的回复格式(相当于圣旨的格式)
struct Response: Decodable {
let content: [Content] // 回复内容(可能是话,也可能是让调用工具)
}
// 消息内容的类型(相当于话的种类:说人话、叫工具、工具结果)
enum Content: Codable {
case text(text: String) // 说人话(比如"我来查查您的发明")
case toolUse(id: String, name: String, input: [String: String]) // 叫工具(比如"调用查发明的工具")
case toolResult(toolUseId: String, content: String) // 工具结果(比如"发明状态是XXX")
// 编码解码用的Key(跟军师约定好的名字)
private enum CodingKeys: String, CodingKey {
case type, text, id, name, input, tool_use_id, content
}
// 解码:把军师的回复转成咱们的Content类型
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
switch type {
case "text":
let text = try container.decode(String.self, forKey: .text)
self = .text(text: text)
case "tool_use":
let id = try container.decode(String.self, forKey: .id)
let name = try container.decode(String.self, forKey: .name)
let input = try container.decode([String: String].self, forKey: .input)
self = .toolUse(id: id, name: name, input: input)
case "tool_result":
let toolUseId = try container.decode(String.self, forKey: .tool_use_id)
let content = try container.decode(String.self, forKey: .content)
self = .toolResult(toolUseId: toolUseId, content: content)
default:
throw ContentError.unknownType // 错误:不知道的内容类型
}
}
// 编码:把咱们的Content转成军师能懂的格式
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let text):
try container.encode("text", forKey: .type)
try container.encode(text, forKey: .text)
case .toolUse(let id, let name, let input):
try container.encode("tool_use", forKey: .type)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(input, forKey: .input)
case .toolResult(let toolUseId, let content):
try container.encode("tool_result", forKey: .type)
try container.encode(toolUseId, forKey: .tool_use_id)
try container.encode(content, forKey: .content)
}
}
}
// 内容相关的错误
enum ContentError: Error {
case unknownType // 不知道的内容类型
}
// 军师服务相关的错误
enum AdvisorError: Error {
case invalidURL // 地址无效
}
零零发边写边念叨:"这 API Key 可得藏好,就像我的'要你命三千'设计图一样,不能让东厂拿去!还有啊,这角色可别搞混了 ------ 你是 user,军师是 assistant,弄反了军师就懵了,跟你和琴操姑娘认错人一样尴尬!"

📱 第四回:搭 "聊天棚"!MCP 客户端的门面
有了服务器和军师,还得有个 "门面"------ 让密探能看见聊天记录,能输入问话。
零零发把这门面叫ContentView
,就像他在电影里搭的 "发明展示棚",得直观、好用。
先看 "聊天脑瓜子"(ViewModel)的代码,它负责管聊天逻辑:
swift
import SwiftUI
import Observation
// 聊天视图模型(相当于"聊天棚"的脑瓜子,管逻辑)
@Observable
final class InventionChatViewModel {
var messages: [ChatMessage] = [] // 聊天记录(存所有消息)
var inputText: String = "" // 输入的话(密探要问的)
var isLoading: Bool = false // 是不是在加载(等军师回复)
// 依赖:发明服务器和军师服务
private let inventionServer: MCPServerProtocol
private let advisorService: ClaudeAdvisorService
// 初始化:把服务器和军师连起来
init() {
// 1. 初始化发明服务器(能查发明的)
self.inventionServer = LingLingFaInventionService()
// 2. 初始化军师服务(要给API Key和服务器的工具)
self.advisorService = ClaudeAdvisorService(
apiKey: "YOUR_API_KEY", // 这里要换成你自己的API Key!
tools: self.inventionServer.tools
)
}
// 发送消息的方法(密探点"发送"就走这儿)
func sendMessage() {
// 1. 把密探输入的话做成消息,加到聊天记录里
let userMessage = Request.Message(
role: .user,
content: [.text(text: inputText)] // 密探说的话
)
messages.append(ChatMessage(message: userMessage))
inputText = "" // 清空输入框(说完话总得把嘴擦干净吧)
isLoading = true // 开始加载(告诉密探:等会儿,军师在想呢)
// 2. 把所有聊天记录整理好,发给军师
let allMessages = messages.map(\.message)
// 3. 异步发消息(不然卡着不动,跟等皇上批奏折一样)
Task {
do {
// 给军师发消息,等回复
let advisorResponse = try await advisorService.send(messages: allMessages)
// 把军师的回复做成消息,加到聊天记录里
let assistantMessage = ChatMessage(
message: Request.Message(
role: .assistant,
content: advisorResponse.content
)
)
self.messages.append(assistantMessage)
// 重点:如果军师让调用工具,就去调用(下集细讲!)
for content in advisorResponse.content {
if case .toolUse(let toolId, let toolName, _) = content {
// 这里就是调用工具的入口,下集咱们再把这步打通!
print("军师让调用工具:\(toolName),ID:\(toolId)")
}
}
} catch {
// 出错了,打印错误(实际开发里得提示密探,别让他懵)
print("糟了!出错了:\(error)")
}
isLoading = false // 加载结束(不管成没成,都告诉密探:好了)
}
}
}
// 聊天消息结构体(把军师的回复转成密探能看懂的样子)
struct ChatMessage: Identifiable {
let message: Request.Message // 原始消息
var id: UUID = .init() // 唯一ID(列表要用)
// 把消息内容转成字符串(密探能直接看的)
var contentText: String {
message.content
.map { content in
switch content {
case .text(let text):
return text // 人话直接返回
case .toolUse(_, let toolName, _):
return "军师调用工具:\(toolName)(查发明状态中...)"
case .toolResult(_, let result):
return "工具返回结果:\(result)"
}
}
.joined(separator: "\n") // 多条内容换行
}
}
零零发指着屏幕说:"你看这 UI,密探的消息放右边,军师的放左边,跟茶馆里俩人对坐聊天一样!输入框能写三行,问复杂的问题也不怕,按钮亮着就能点 ------ 比我那'要你命三千'的开关好懂多了!"

🎬 尾声:神器初成,就差最后一步!
话说零零发把 "密探发明聊天棚" 搭好了:能查发明的服务器立住了,能聊天的军师也接上了,UI 门面也弄得明明白白。
现在你要是打开 APP,输入 "查下我那天外飞仙破解器的状态",军师会说 "我来调用工具查查",可查完之后呢?工具的结果还没传给军师,军师也没法给你个完整的答复 ------ 这就像 "要你命三千" 装好了刀片,却没按启动键,砍不了人啊!

下一回,咱们就来打通这最后一步:**让工具调用的结果自动传给军师,军师再给你个清清楚楚的回复。**到时候,你问一句 "我的发明咋样了",神器就能自动查、自动说,比零零发的老婆还贴心!各位客官,咱们下集接着唠,不见不散~
