feat: 2025-11-25 review fixes (#201)
* fix: review fixes of the latest changes * fix: updated unit tests * chore: more conformance compliance improvements * chore: reverted protocol changes * chore: more compliance improvements * chore: more conformance compliance improvements * chore: improvements to the backcompat and updated to Tool unit tests * chore: ci update
This commit is contained in:
parent
6112a3995a
commit
973b46b8f7
4 changed files with 692 additions and 507 deletions
|
|
@ -38,11 +38,27 @@ public actor Client {
|
|||
public var title: String?
|
||||
/// The client version
|
||||
public var version: String
|
||||
/// Optional description of the client
|
||||
public var description: String?
|
||||
/// Optional website URL for the client
|
||||
public var websiteUrl: String?
|
||||
/// Optional set of sized icons for display in a user interface
|
||||
public var icons: [Icon]?
|
||||
|
||||
public init(name: String, version: String, title: String? = nil) {
|
||||
public init(
|
||||
name: String,
|
||||
version: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
websiteUrl: String? = nil,
|
||||
icons: [Icon]? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.websiteUrl = websiteUrl
|
||||
self.icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,46 +179,6 @@ public actor Client {
|
|||
/// The task for the message handling loop
|
||||
private var task: Task<Void, Never>?
|
||||
|
||||
/// An error indicating a type mismatch when decoding a pending request
|
||||
private struct TypeMismatchError: Swift.Error {}
|
||||
|
||||
/// A pending request with a continuation for the result
|
||||
private struct PendingRequest<T> {
|
||||
let continuation: CheckedContinuation<T, Swift.Error>
|
||||
}
|
||||
|
||||
/// A type-erased pending request
|
||||
private struct AnyPendingRequest: Sendable {
|
||||
private let _resume: @Sendable (Result<Any, Swift.Error>) -> Void
|
||||
|
||||
init<T: Sendable & Decodable>(_ request: PendingRequest<T>) {
|
||||
_resume = { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
if let typedValue = value as? T {
|
||||
request.continuation.resume(returning: typedValue)
|
||||
} else if let value = value as? Value,
|
||||
let data = try? JSONEncoder().encode(value),
|
||||
let decoded = try? JSONDecoder().decode(T.self, from: data)
|
||||
{
|
||||
request.continuation.resume(returning: decoded)
|
||||
} else {
|
||||
request.continuation.resume(throwing: TypeMismatchError())
|
||||
}
|
||||
case .failure(let error):
|
||||
request.continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
func resume(returning value: Any) {
|
||||
_resume(.success(value))
|
||||
}
|
||||
|
||||
func resume(throwing error: Swift.Error) {
|
||||
_resume(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// A dictionary of type-erased pending requests, keyed by request ID
|
||||
private var pendingRequests: [ID: AnyPendingRequest] = [:]
|
||||
// Add reusable JSON encoder/decoder
|
||||
|
|
@ -213,10 +189,15 @@ public actor Client {
|
|||
name: String,
|
||||
version: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
websiteUrl: String? = nil,
|
||||
icons: [Icon]? = nil,
|
||||
capabilities: Capabilities = Capabilities(),
|
||||
configuration: Configuration = .default
|
||||
) {
|
||||
self.clientInfo = Client.Info(name: name, version: version, title: title)
|
||||
self.clientInfo = Client.Info(
|
||||
name: name, version: version, title: title,
|
||||
description: description, websiteUrl: websiteUrl, icons: icons)
|
||||
self.capabilities = capabilities
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
|
@ -285,13 +266,15 @@ public actor Client {
|
|||
await self.logger?.debug(
|
||||
"Received cancellation notification",
|
||||
metadata: [
|
||||
"requestId": "\(requestId)",
|
||||
"requestId": requestId.map { "\($0)" } ?? "none",
|
||||
"reason": reason.map { "\($0)" } ?? "none",
|
||||
]
|
||||
)
|
||||
|
||||
// Remove the pending request and resume with cancellation error
|
||||
if let pendingRequest = await self.removePendingRequest(id: requestId) {
|
||||
if let requestId = requestId,
|
||||
let pendingRequest = await self.removePendingRequest(id: requestId)
|
||||
{
|
||||
pendingRequest.resume(throwing: CancellationError())
|
||||
}
|
||||
}
|
||||
|
|
@ -671,6 +654,12 @@ public actor Client {
|
|||
self.serverVersion = result.protocolVersion
|
||||
self.instructions = result.instructions
|
||||
|
||||
// For HTTP transport, ensure subsequent MCP-Protocol-Version headers
|
||||
// reflect the negotiated lifecycle version.
|
||||
if let httpTransport = connection as? HTTPClientTransport {
|
||||
await httpTransport.updateNegotiatedProtocolVersion(result.protocolVersion)
|
||||
}
|
||||
|
||||
try await notify(InitializedNotification.message())
|
||||
|
||||
return result
|
||||
|
|
@ -683,7 +672,7 @@ public actor Client {
|
|||
|
||||
// MARK: - Prompts
|
||||
|
||||
public func getPrompt(name: String, arguments: [String: Value]? = nil) async throws
|
||||
public func getPrompt(name: String, arguments: [String: String]? = nil) async throws
|
||||
-> (description: String?, messages: [Prompt.Message])
|
||||
{
|
||||
try validateServerCapability(\.prompts, "Prompts")
|
||||
|
|
@ -923,7 +912,7 @@ public actor Client {
|
|||
promptName: String,
|
||||
argumentName: String,
|
||||
argumentValue: String,
|
||||
context: [String: Value]? = nil
|
||||
context: [String: String]? = nil
|
||||
) async throws -> Complete.Result.Completion {
|
||||
try validateServerCapability(\.completions, "Completions")
|
||||
let request = Complete.request(
|
||||
|
|
@ -954,7 +943,7 @@ public actor Client {
|
|||
resourceURI: String,
|
||||
argumentName: String,
|
||||
argumentValue: String,
|
||||
context: [String: Value]? = nil
|
||||
context: [String: String]? = nil
|
||||
) async throws -> Complete.Result.Completion {
|
||||
try validateServerCapability(\.completions, "Completions")
|
||||
let request = Complete.request(
|
||||
|
|
|
|||
|
|
@ -68,15 +68,15 @@ public enum CreateElicitation: Method {
|
|||
public var message: String
|
||||
/// Elicitation mode (optional for backward compatibility, defaults to form)
|
||||
public var mode: Elicitation.Mode?
|
||||
/// Optional schema describing the expected response content
|
||||
public var requestedSchema: Elicitation.RequestSchema?
|
||||
/// Schema describing the expected response content (required per spec)
|
||||
public var requestedSchema: Elicitation.RequestSchema
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
message: String,
|
||||
mode: Elicitation.Mode? = nil,
|
||||
requestedSchema: Elicitation.RequestSchema? = nil,
|
||||
requestedSchema: Elicitation.RequestSchema = .init(),
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.message = message
|
||||
|
|
@ -188,7 +188,7 @@ extension CreateElicitation.Parameters: Codable {
|
|||
} else {
|
||||
// Form mode (default for backward compatibility)
|
||||
let message = try container.decode(String.self, forKey: .message)
|
||||
let requestedSchema = try container.decodeIfPresent(
|
||||
let requestedSchema = try container.decode(
|
||||
Elicitation.RequestSchema.self, forKey: .requestedSchema)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .form(FormParameters(
|
||||
|
|
|
|||
|
|
@ -16,30 +16,37 @@ public struct Root: Hashable, Codable, Sendable {
|
|||
public let uri: String
|
||||
/// Optional human-readable name for the root
|
||||
public let name: String?
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
uri: String,
|
||||
name: String? = nil
|
||||
name: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.uri = uri
|
||||
self.name = name
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uri
|
||||
case name
|
||||
case _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
uri = try container.decode(String.self, forKey: .uri)
|
||||
name = try container.decodeIfPresent(String.self, forKey: .name)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encodeIfPresent(name, forKey: .name)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import Foundation
|
|||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
|
||||
public enum Sampling {
|
||||
/// A message in the conversation history.
|
||||
public struct Message: Hashable, Codable, Sendable {
|
||||
public struct Message: Hashable, Sendable {
|
||||
/// The message role
|
||||
public enum Role: String, Hashable, Codable, Sendable {
|
||||
/// A user message
|
||||
|
|
@ -20,30 +20,34 @@ public enum Sampling {
|
|||
public let role: Role
|
||||
/// The message content
|
||||
public let content: Content
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
/// Creates a message with the specified role and content
|
||||
@available(
|
||||
*, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead"
|
||||
)
|
||||
public init(role: Role, content: Content) {
|
||||
public init(role: Role, content: Content, _meta: Metadata? = nil) {
|
||||
self.role = role
|
||||
self.content = content
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
/// Private initializer for convenience methods to avoid deprecation warnings
|
||||
private init(_role role: Role, _content content: Content) {
|
||||
private init(_role role: Role, _content content: Content, _meta: Metadata? = nil) {
|
||||
self.role = role
|
||||
self.content = content
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
/// Creates a user message with the specified content
|
||||
public static func user(_ content: Content) -> Message {
|
||||
return Message(_role: .user, _content: content)
|
||||
public static func user(_ content: Content, _meta: Metadata? = nil) -> Message {
|
||||
return Message(_role: .user, _content: content, _meta: _meta)
|
||||
}
|
||||
|
||||
/// Creates an assistant message with the specified content
|
||||
public static func assistant(_ content: Content) -> Message {
|
||||
return Message(_role: .assistant, _content: content)
|
||||
public static func assistant(_ content: Content, _meta: Metadata? = nil) -> Message {
|
||||
return Message(_role: .assistant, _content: content, _meta: _meta)
|
||||
}
|
||||
|
||||
/// Content types for sampling messages
|
||||
|
|
@ -144,16 +148,25 @@ public enum Sampling {
|
|||
case allServers
|
||||
}
|
||||
|
||||
/// Stop reason for sampling completion
|
||||
public enum StopReason: String, Hashable, Codable, Sendable {
|
||||
/// Stop reason for sampling completion.
|
||||
///
|
||||
/// The spec defines this as an open string — any provider-specific value is valid.
|
||||
/// The well-known values are exposed as static constants.
|
||||
public struct StopReason: RawRepresentable, Hashable, Codable, Sendable,
|
||||
ExpressibleByStringLiteral
|
||||
{
|
||||
public let rawValue: String
|
||||
public init(rawValue: String) { self.rawValue = rawValue }
|
||||
public init(stringLiteral value: String) { self.rawValue = value }
|
||||
|
||||
/// Natural end of turn
|
||||
case endTurn
|
||||
public static let endTurn = StopReason(rawValue: "endTurn")
|
||||
/// Hit a stop sequence
|
||||
case stopSequence
|
||||
public static let stopSequence = StopReason(rawValue: "stopSequence")
|
||||
/// Reached maximum tokens
|
||||
case maxTokens
|
||||
public static let maxTokens = StopReason(rawValue: "maxTokens")
|
||||
/// Model wants to use a tool
|
||||
case toolUse
|
||||
public static let toolUse = StopReason(rawValue: "toolUse")
|
||||
}
|
||||
|
||||
/// Content representing a tool use request from the model
|
||||
|
|
@ -220,6 +233,26 @@ public enum Sampling {
|
|||
|
||||
// MARK: - Codable
|
||||
|
||||
extension Sampling.Message: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case role, content, _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
role = try container.decode(Role.self, forKey: .role)
|
||||
content = try container.decode(Content.self, forKey: .content)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(role, forKey: .role)
|
||||
try container.encode(content, forKey: .content)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
|
||||
extension Sampling.Message.Content.ContentBlock: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, text, data, mimeType
|
||||
|
|
@ -243,13 +276,13 @@ extension Sampling.Message.Content.ContentBlock: Codable {
|
|||
let data = try container.decode(String.self, forKey: .data)
|
||||
let mimeType = try container.decode(String.self, forKey: .mimeType)
|
||||
self = .audio(data: data, mimeType: mimeType)
|
||||
case "toolUse":
|
||||
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: Value].self, forKey: .input)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .toolUse(Sampling.ToolUseContent(id: id, name: name, input: input, _meta: _meta))
|
||||
case "toolResult":
|
||||
case "tool_result":
|
||||
let toolUseId = try container.decode(String.self, forKey: .toolUseId)
|
||||
let content = try container.decode([Sampling.ToolResultContent.ContentBlock].self, forKey: .content)
|
||||
let structuredContent = try container.decodeIfPresent([String: Value].self, forKey: .structuredContent)
|
||||
|
|
@ -284,13 +317,13 @@ extension Sampling.Message.Content.ContentBlock: Codable {
|
|||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
case .toolUse(let toolUse):
|
||||
try container.encode("toolUse", forKey: .type)
|
||||
try container.encode("tool_use", forKey: .type)
|
||||
try container.encode(toolUse.id, forKey: .id)
|
||||
try container.encode(toolUse.name, forKey: .name)
|
||||
try container.encode(toolUse.input, forKey: .input)
|
||||
try container.encodeIfPresent(toolUse._meta, forKey: ._meta)
|
||||
case .toolResult(let toolResult):
|
||||
try container.encode("toolResult", forKey: .type)
|
||||
try container.encode("tool_result", forKey: .type)
|
||||
try container.encode(toolResult.toolUseId, forKey: .toolUseId)
|
||||
try container.encode(toolResult.content, forKey: .content)
|
||||
try container.encodeIfPresent(toolResult.structuredContent, forKey: .structuredContent)
|
||||
|
|
@ -372,7 +405,7 @@ extension Sampling.ToolResultContent.ContentBlock: Codable {
|
|||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .resource(resource: resourceContent, annotations: annotations, _meta: _meta)
|
||||
case "resourceLink":
|
||||
case "resource_link":
|
||||
let uri = try container.decode(String.self, forKey: .uri)
|
||||
let name = try container.decode(String.self, forKey: .name)
|
||||
let title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
|
|
@ -410,7 +443,7 @@ extension Sampling.ToolResultContent.ContentBlock: Codable {
|
|||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
case .resourceLink(let uri, let name, let title, let description, let mimeType, let annotations):
|
||||
try container.encode("resourceLink", forKey: .type)
|
||||
try container.encode("resource_link", forKey: .type)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
|
|
@ -440,12 +473,28 @@ public enum CreateSamplingMessage: Method {
|
|||
case none
|
||||
}
|
||||
|
||||
/// The tool choice mode
|
||||
public let mode: Mode
|
||||
/// The tool choice mode. If omitted, defaults to `.auto`.
|
||||
public let mode: Mode?
|
||||
|
||||
public init(mode: Mode) {
|
||||
public init(mode: Mode? = .auto) {
|
||||
self.mode = mode
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case mode
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
mode = try container.decodeIfPresent(Mode.self, forKey: .mode) ?? .auto
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
if let mode, mode != .auto {
|
||||
try container.encode(mode, forKey: .mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
|
|
@ -469,6 +518,8 @@ public enum CreateSamplingMessage: Method {
|
|||
public let tools: [Tool]?
|
||||
/// Tool choice configuration
|
||||
public let toolChoice: ToolChoice?
|
||||
/// Provider-specific metadata to pass to the LLM
|
||||
public let metadata: [String: Value]?
|
||||
|
||||
public init(
|
||||
messages: [Sampling.Message],
|
||||
|
|
@ -480,7 +531,8 @@ public enum CreateSamplingMessage: Method {
|
|||
stopSequences: [String]? = nil,
|
||||
_meta: Metadata? = nil,
|
||||
tools: [Tool]? = nil,
|
||||
toolChoice: ToolChoice? = nil
|
||||
toolChoice: ToolChoice? = nil,
|
||||
metadata: [String: Value]? = nil
|
||||
) {
|
||||
self.messages = messages
|
||||
self.modelPreferences = modelPreferences
|
||||
|
|
@ -492,6 +544,7 @@ public enum CreateSamplingMessage: Method {
|
|||
self._meta = _meta
|
||||
self.tools = tools
|
||||
self.toolChoice = toolChoice
|
||||
self.metadata = metadata
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue