You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

366 lines
12 KiB

import Foundation
/// 工具类型枚举
public enum ToolType {
case localFunction // 本地函数
case mcpTool // MCP工具
}
/// 函数处理器协议
public protocol FunctionHandler {
/// 处理函数调用
/// - Parameter arguments: 函数参数,字典格式
/// - Returns: 函数执行结果,字符串
func handle(arguments: [String: Any]) async throws -> String
}
/// 单个MCP子客户端
/// 负责连接到一个具体的MCP服务器并管理其工具
public class MCPSubClient {
private let tag: String
private let serverId: String
private let serverUrl: String
// 服务器提供的工具列表
private var tools: [[String: Any]] = []
// 工具Map列表,用于OpenAI请求
private var toolMaps: [[String: Any]] = []
// 是否已连接
private var isConnectedFlag = false
// 工具名称到工具的映射,用于快速查找
private var toolMapping: [String: [String: Any]] = [:]
// URL会话
private lazy var urlSession: URLSession = {
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 30
return URLSession(configuration: config)
}()
public init(serverId: String, serverUrl: String) {
self.serverId = serverId
self.serverUrl = serverUrl
self.tag = "MCPSubClient[\(serverId)]"
print("\(tag) 初始化,URL: \(serverUrl)")
}
/// 连接到MCP服务器(通过SSE)
/// - Returns: 是否连接成功
public func connect() async -> Bool {
do {
print("\(tag) 开始连接服务器: \(serverUrl)")
// 这里应该实现实际的SSE连接逻辑
// 由于iOS没有直接的SSE库,我们可以使用URLSession来模拟
// 或者使用第三方库如EventSource
// 模拟连接成功并获取工具列表
await processToolsList()
isConnectedFlag = true
print("\(tag) 连接成功,可用工具: \(tools.count)个")
return true
} catch {
print("\(tag) 连接失败: \(error.localizedDescription)")
return false
}
}
/// 处理工具列表
private func processToolsList() async {
// 这里应该从MCP服务器获取实际的工具列表
// 目前先模拟一些基本工具
tools = []
// 更新工具映射
toolMapping.removeAll()
for tool in tools {
if let name = tool["name"] as? String {
toolMapping[name] = tool
}
}
// 更新工具Map列表
updateToolMaps()
print("\(tag) 成功处理 \(tools.count) 个工具")
}
/// 更新工具Map列表,用于OpenAI请求
/// 将MCP工具转换为OpenAI函数调用格式
private func updateToolMaps() {
toolMaps.removeAll()
for tool in tools {
guard let name = tool["name"] as? String,
let description = tool["description"] as? String else {
continue
}
let functionMap: [String: Any] = [
"name": name,
"description": description,
"parameters": [
"type": "object",
"properties": [:],
"required": []
]
]
toolMaps.append([
"type": "function",
"function": functionMap
])
}
print("\(tag) 更新工具映射完成,共 \(toolMaps.count) 个工具")
}
/// 获取工具Map列表,用于OpenAI请求
public func getToolMaps() -> [[String: Any]] {
return toolMaps
}
/// 获取可用的工具列表
public func getTools() -> [[String: Any]] {
return tools
}
/// 检查是否包含指定工具
public func containsTool(name: String) -> Bool {
return toolMapping.keys.contains(name)
}
/// 调用工具
public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] {
guard isConnectedFlag else {
print("\(tag) 未连接到服务器")
return ["context": "未连接到MCP服务器"]
}
guard containsTool(name: name) else {
print("\(tag) 此客户端不包含工具: \(name)")
throw OpenAIException("工具不存在: \(name)")
}
// 这里应该实现实际的工具调用逻辑
// 目前先返回模拟结果
return [
"name": name,
"context": "工具调用结果: \(name)"
]
}
/// 检查是否已连接
public func isConnected() -> Bool {
return isConnectedFlag
}
/// 关闭连接
public func close() {
isConnectedFlag = false
print("\(tag) 已关闭MCP服务器连接")
}
}
/// MCP客户端,用于连接Model Context Protocol服务器和管理本地函数
public class MCPClient {
private let tag = "MCPClient"
// 本地函数Map,函数名 -> 处理器
private var localFunctions: [String: FunctionHandler] = [:]
// 本地函数定义Map,函数名 -> 定义
private var localFunctionDefs: [String: String] = [:]
// 子客户端列表,每个连接一个MCP服务器
private var subClients: [String: MCPSubClient] = [:]
// 是否已连接
private var isConnectedFlag = false
// 系统功能处理器
private var systemFunctionHandler: SystemFunctionHandler?
public init() {
// 初始化系统功能处理器
systemFunctionHandler = SystemFunctionHandler()
initializeVoiceFunctions()
}
/// 初始化语音功能处理器
private func initializeVoiceFunctions() {
do {
// 使用SystemFunctionHandler注册所有功能
systemFunctionHandler?.registerAllFunctions(client: self)
print("\(tag) 语音功能初始化完成")
} catch {
print("\(tag) 语音功能初始化失败: \(error.localizedDescription)")
}
}
/// 连接到MCP服务器(通过SSE)
/// 直接接收完整的JSON配置字符串
/// - Parameter mcpConfigJson: 包含mcpServers字段的JSON配置字符串
/// - Returns: 是否连接成功
public func connectToSSE(mcpConfigJson: String) async throws -> Bool {
// 清除现有连接
closeAllConnections()
var serverMap: [String: String] = [:]
var configLoaded = false
// 尝试解析传入的JSON配置
if !mcpConfigJson.isEmpty {
do {
guard let data = mcpConfigJson.data(using: .utf8),
let config = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let mcpServers = config["mcpServers"] as? [String: [String: String]] else {
throw OpenAIException("无效的MCP配置格式")
}
// 从服务器配置中提取URL
for (serverId, serverConfig) in mcpServers {
if let url = serverConfig["url"], !url.isEmpty {
serverMap[serverId] = url
print("\(tag) 从配置中添加服务器 [\(serverId)]: \(url)")
}
}
configLoaded = !serverMap.isEmpty
} catch {
print("\(tag) 解析MCP配置失败: \(error.localizedDescription)")
}
}
// 如果没有从配置中加载到服务器,使用默认配置
if !configLoaded {
print("\(tag) 使用默认MCP服务器配置")
// 可以在这里添加默认的服务器配置
}
// 连接到所有服务器
var successCount = 0
for (serverId, serverUrl) in serverMap {
let subClient = MCPSubClient(serverId: serverId, serverUrl: serverUrl)
let connected = await subClient.connect()
if connected {
subClients[serverId] = subClient
successCount += 1
print("\(tag) 成功连接到服务器 [\(serverId)]")
} else {
print("\(tag) 连接服务器失败 [\(serverId)]")
}
}
isConnectedFlag = successCount > 0
print("\(tag) MCP客户端连接完成,成功连接 \(successCount)/\(serverMap.count) 个服务器")
return isConnectedFlag
}
/// 关闭所有连接
private func closeAllConnections() {
for (_, subClient) in subClients {
subClient.close()
}
subClients.removeAll()
isConnectedFlag = false
}
/// 获取工具Map列表,用于OpenAI请求
public func getToolMaps() -> [[String: Any]] {
var allToolMaps: [[String: Any]] = []
// 添加所有子客户端的工具
for (_, subClient) in subClients {
allToolMaps.append(contentsOf: subClient.getToolMaps())
}
// 添加本地函数
for (name, _) in localFunctions {
if let def = localFunctionDefs[name],
let data = def.data(using: .utf8),
let functionDef = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
allToolMaps.append([
"type": "function",
"function": functionDef
])
}
}
return allToolMaps
}
/// 注册本地函数
public func registerLocalFunction(name: String, description: String, parameters: [String: Any], handler: FunctionHandler) -> Bool {
do {
localFunctions[name] = handler
let functionDef: [String: Any] = [
"name": name,
"description": description,
"parameters": parameters
]
let jsonData = try JSONSerialization.data(withJSONObject: functionDef)
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
localFunctionDefs[name] = jsonString
// print("\(tag) 成功注册本地函数: \(name)")
return true
} catch {
print("\(tag) 注册本地函数失败: \(error.localizedDescription)")
return false
}
}
/// 调用工具
public func callTool(name: String, arguments: [String: Any]) async throws -> [String: Any] {
// 首先检查是否是本地函数
if let handler = localFunctions[name] {
do {
let result = try await handler.handle(arguments: arguments)
return ["context": result]
} catch {
throw OpenAIException("本地函数调用失败: \(error.localizedDescription)")
}
}
// 然后检查MCP工具
for (_, subClient) in subClients {
if subClient.containsTool(name: name) {
return try await subClient.callTool(name: name, arguments: arguments)
}
}
throw OpenAIException("未找到工具: \(name)")
}
/// 解析JSON参数
public func parseJsonArguments(_ argumentsJson: String) throws -> [String: Any] {
guard let data = argumentsJson.data(using: .utf8),
let arguments = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return [:]
}
return arguments
}
/// 检查是否已连接
public func isConnected() -> Bool {
return isConnectedFlag || !localFunctions.isEmpty
}
/// 关闭连接
public func close() {
closeAllConnections()
localFunctions.removeAll()
localFunctionDefs.removeAll()
print("\(tag) 已关闭MCP客户端")
}
}