* Support 2025-06-18 Spec (#167) * Support OpenAI _meta for Tool * Update init * Add title and outputSchema * Concurreny issue * General fields * Add Generate Fields to Server elements * 4 tab spacing * Update Version * Elicitation * Format * More tests and inits * Elicitation in README * Fix orphaned test * Add title to more structs * Public init * Fix meta validation and make tests more generic * Format and make Result properties public again * Revert more visibility changes * feat: 2025-11-25 phase 1, includes Version, Icon, Progress (#181) * chore: fixed unit tests * feat: added icon and progess base structs * feat: added progress notification and meta to callTool * feat: added tests * feat: simplified RequestMeta encoding/decoding * tests: added unit test to track progress * chore: rebased v0.11.x * feat: implemented cancellation * feat: added overloaded callTool method * fix: encoding Response _meta fix * chore: added old version to the versions list * feat: 2025-11-25 phase 2; added Logging and Completions (#196) * feat: added Logging and Completions * feat: updated EmbeddedResource for the latest spec * chore: changed encoding of meta in Prompts to encodeIfPresent * feat: compliance tests + http server transport (#197) * feat: http server transport * feat: added http server transport * feat: removed unused files * feat: added conformance tests * feat: improved conformace testing * chore: removed redundant files * feat/2025-11-25: sampling, elicitation, roots (#198) * feat: Roots implementation * feat: added unit testing * feat: improved unit tests and fixed NetworkTransport crash * feat: implemented sampling and elicitation * feat: improved sampling tests * feat: improved elicitation tests * feat: improved confirmance tests * feat: improved Prompts unit tests * feat: removed test code * feat: updated conformance tests * chore: removed redundant commas * chore: removed 6.0.3 swift version from ci * chore: improved unit tests a little bit * chore: update README.md * chore: add server progress tracking example to README.md * chore: updated README.md * chore: updated transports table formatting * chore: updated MockNetworkConnection --------- Co-authored-by: Austin Evans <austinjevans@me.com> Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
447 lines
15 KiB
Swift
447 lines
15 KiB
Swift
import class Foundation.JSONDecoder
|
|
import class Foundation.JSONEncoder
|
|
|
|
private let jsonrpc = "2.0"
|
|
|
|
public protocol NotRequired {
|
|
init()
|
|
}
|
|
|
|
public struct Empty: NotRequired, Hashable, Codable, Sendable {
|
|
public init() {}
|
|
}
|
|
|
|
extension Value: NotRequired {
|
|
public init() {
|
|
self = .null
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
/// A method that can be used to send requests and receive responses.
|
|
public protocol Method: Sendable {
|
|
/// The parameters of the method.
|
|
associatedtype Parameters: Codable, Hashable, Sendable = Empty
|
|
/// The result of the method.
|
|
associatedtype Result: Codable, Hashable, Sendable = Empty
|
|
/// The name of the method.
|
|
static var name: String { get }
|
|
}
|
|
|
|
/// Type-erased method for request/response handling
|
|
struct AnyMethod: Method, Sendable {
|
|
static var name: String { "" }
|
|
typealias Parameters = Value
|
|
typealias Result = Value
|
|
}
|
|
|
|
extension Method where Parameters == Empty {
|
|
public static func request(
|
|
id: ID = .random,
|
|
_meta: Metadata? = nil
|
|
) -> Request<Self> {
|
|
Request(id: id, method: name, params: Empty(), _meta: _meta)
|
|
}
|
|
}
|
|
|
|
extension Method where Result == Empty {
|
|
public static func response(id: ID) -> Response<Self> {
|
|
Response(id: id, result: Empty())
|
|
}
|
|
}
|
|
|
|
extension Method {
|
|
/// Create a request with the given parameters.
|
|
public static func request(
|
|
id: ID = .random,
|
|
_ parameters: Self.Parameters,
|
|
_meta: Metadata? = nil
|
|
) -> Request<Self> {
|
|
Request(id: id, method: name, params: parameters, _meta: _meta)
|
|
}
|
|
|
|
/// Create a response with the given result.
|
|
public static func response(
|
|
id: ID,
|
|
result: Self.Result,
|
|
_meta: Metadata? = nil
|
|
) -> Response<Self> {
|
|
Response(id: id, result: result, _meta: _meta)
|
|
}
|
|
|
|
/// Create a response with the given error.
|
|
public static func response(
|
|
id: ID,
|
|
error: MCPError,
|
|
_meta: Metadata? = nil
|
|
) -> Response<Self> {
|
|
Response(id: id, error: error, _meta: _meta)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
/// A request message.
|
|
public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
|
|
/// The request ID.
|
|
public let id: ID
|
|
/// The method name.
|
|
public let method: String
|
|
/// The request parameters.
|
|
public let params: M.Parameters
|
|
/// Metadata for this request (see spec for _meta usage, includes progressToken)
|
|
public let _meta: Metadata?
|
|
|
|
init(
|
|
id: ID = .random,
|
|
method: String,
|
|
params: M.Parameters,
|
|
_meta: Metadata? = nil
|
|
) {
|
|
self.id = id
|
|
self.method = method
|
|
self.params = params
|
|
self._meta = _meta
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey, CaseIterable {
|
|
case jsonrpc, id, method, params, _meta
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(jsonrpc, forKey: .jsonrpc)
|
|
try container.encode(id, forKey: .id)
|
|
try container.encode(method, forKey: .method)
|
|
try container.encode(params, forKey: .params)
|
|
try container.encodeIfPresent(_meta, forKey: ._meta)
|
|
}
|
|
}
|
|
|
|
extension Request {
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let version = try container.decode(String.self, forKey: .jsonrpc)
|
|
guard version == jsonrpc else {
|
|
throw DecodingError.dataCorruptedError(
|
|
forKey: .jsonrpc, in: container, debugDescription: "Invalid JSON-RPC version")
|
|
}
|
|
id = try container.decode(ID.self, forKey: .id)
|
|
method = try container.decode(String.self, forKey: .method)
|
|
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
|
|
|
if M.Parameters.self is NotRequired.Type {
|
|
// For NotRequired parameters, use decodeIfPresent or init()
|
|
params =
|
|
(try container.decodeIfPresent(M.Parameters.self, forKey: .params)
|
|
?? (M.Parameters.self as! NotRequired.Type).init() as! M.Parameters)
|
|
} else if let value = try? container.decode(M.Parameters.self, forKey: .params) {
|
|
// If params exists and can be decoded, use it
|
|
params = value
|
|
} else if !container.contains(.params)
|
|
|| (try? container.decodeNil(forKey: .params)) == true
|
|
{
|
|
// If params is missing or explicitly null, use Empty for Empty parameters
|
|
// or throw for non-Empty parameters
|
|
if M.Parameters.self == Empty.self {
|
|
params = Empty() as! M.Parameters
|
|
} else {
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: container.codingPath,
|
|
debugDescription: "Missing required params field"))
|
|
}
|
|
} else {
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: container.codingPath,
|
|
debugDescription: "Invalid params field"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A type-erased request for request/response handling
|
|
typealias AnyRequest = Request<AnyMethod>
|
|
|
|
extension AnyRequest {
|
|
init<T: Method>(_ request: Request<T>) throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let data = try encoder.encode(request)
|
|
self = try decoder.decode(AnyRequest.self, from: data)
|
|
}
|
|
}
|
|
|
|
/// A box for request handlers that can be type-erased
|
|
class RequestHandlerBox: @unchecked Sendable {
|
|
func callAsFunction(_ request: AnyRequest) async throws -> AnyResponse {
|
|
fatalError("Must override")
|
|
}
|
|
}
|
|
|
|
/// A typed request handler that can be used to handle requests of a specific type
|
|
final class TypedRequestHandler<M: Method>: RequestHandlerBox, @unchecked Sendable {
|
|
private let _handle: @Sendable (Request<M>) async throws -> Response<M>
|
|
|
|
init(_ handler: @escaping @Sendable (Request<M>) async throws -> Response<M>) {
|
|
self._handle = handler
|
|
super.init()
|
|
}
|
|
|
|
override func callAsFunction(_ request: AnyRequest) async throws -> AnyResponse {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
// Create a concrete request from the type-erased one
|
|
let data = try encoder.encode(request)
|
|
let request = try decoder.decode(Request<M>.self, from: data)
|
|
|
|
// Handle with concrete type
|
|
let response = try await _handle(request)
|
|
|
|
// Convert result to AnyMethod response
|
|
switch response.result {
|
|
case .success(let result):
|
|
let resultData = try encoder.encode(result)
|
|
let resultValue = try decoder.decode(Value.self, from: resultData)
|
|
return Response(id: response.id, result: resultValue)
|
|
case .failure(let error):
|
|
return Response(id: response.id, error: error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
/// A response message.
|
|
public struct Response<M: Method>: Hashable, Identifiable, Codable, Sendable {
|
|
/// The response ID.
|
|
public let id: ID
|
|
/// The response result.
|
|
public let result: Swift.Result<M.Result, MCPError>
|
|
/// Metadata for this response (see spec for _meta usage)
|
|
public let _meta: Metadata?
|
|
|
|
public init(
|
|
id: ID,
|
|
result: Swift.Result<M.Result, MCPError>,
|
|
_meta: Metadata? = nil
|
|
) {
|
|
self.id = id
|
|
self.result = result
|
|
self._meta = _meta
|
|
}
|
|
|
|
public init(
|
|
id: ID,
|
|
result: M.Result,
|
|
_meta: Metadata? = nil
|
|
) {
|
|
self.init(id: id, result: .success(result), _meta: _meta)
|
|
}
|
|
|
|
public init(
|
|
id: ID,
|
|
error: MCPError,
|
|
_meta: Metadata? = nil
|
|
) {
|
|
self.init(id: id, result: .failure(error), _meta: _meta)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey, CaseIterable {
|
|
case jsonrpc, id, result, error, _meta
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(jsonrpc, forKey: .jsonrpc)
|
|
try container.encode(id, forKey: .id)
|
|
try container.encodeIfPresent(_meta, forKey: ._meta)
|
|
|
|
switch result {
|
|
case .success(let result):
|
|
try container.encode(result, forKey: .result)
|
|
case .failure(let error):
|
|
try container.encode(error, forKey: .error)
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let version = try container.decode(String.self, forKey: .jsonrpc)
|
|
guard version == jsonrpc else {
|
|
throw DecodingError.dataCorruptedError(
|
|
forKey: .jsonrpc, in: container, debugDescription: "Invalid JSON-RPC version")
|
|
}
|
|
id = try container.decode(ID.self, forKey: .id)
|
|
if let result = try? container.decode(M.Result.self, forKey: .result) {
|
|
self.result = .success(result)
|
|
} else if let error = try? container.decode(MCPError.self, forKey: .error) {
|
|
self.result = .failure(error)
|
|
} else {
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: container.codingPath,
|
|
debugDescription: "Invalid response"))
|
|
}
|
|
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
|
}
|
|
}
|
|
|
|
/// A type-erased response for request/response handling
|
|
typealias AnyResponse = Response<AnyMethod>
|
|
|
|
extension AnyResponse {
|
|
init<T: Method>(_ response: Response<T>) throws {
|
|
switch response.result {
|
|
case .success(let result):
|
|
let data = try JSONEncoder().encode(result)
|
|
let resultValue = try JSONDecoder().decode(Value.self, from: data)
|
|
self = Response<AnyMethod>(
|
|
id: response.id,
|
|
result: .success(resultValue),
|
|
_meta: response._meta
|
|
)
|
|
case .failure(let error):
|
|
self = Response<AnyMethod>(
|
|
id: response.id,
|
|
result: .failure(error),
|
|
_meta: response._meta
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
/// A notification message.
|
|
public protocol Notification: Hashable, Codable, Sendable {
|
|
/// The parameters of the notification.
|
|
associatedtype Parameters: Hashable, Codable, Sendable = Empty
|
|
/// The name of the notification.
|
|
static var name: String { get }
|
|
}
|
|
|
|
/// A type-erased notification for message handling
|
|
struct AnyNotification: Notification, Sendable {
|
|
static var name: String { "" }
|
|
typealias Parameters = Value
|
|
}
|
|
|
|
extension AnyNotification {
|
|
init(_ notification: some Notification) throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let data = try encoder.encode(notification)
|
|
self = try decoder.decode(AnyNotification.self, from: data)
|
|
}
|
|
}
|
|
|
|
/// A message that can be used to send notifications.
|
|
public struct Message<N: Notification>: Hashable, Codable, Sendable {
|
|
/// The method name.
|
|
public let method: String
|
|
/// The notification parameters.
|
|
public let params: N.Parameters
|
|
|
|
public init(method: String, params: N.Parameters) {
|
|
self.method = method
|
|
self.params = params
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case jsonrpc, method, params
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(jsonrpc, forKey: .jsonrpc)
|
|
try container.encode(method, forKey: .method)
|
|
if N.Parameters.self != Empty.self {
|
|
try container.encode(params, forKey: .params)
|
|
}
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let version = try container.decode(String.self, forKey: .jsonrpc)
|
|
guard version == jsonrpc else {
|
|
throw DecodingError.dataCorruptedError(
|
|
forKey: .jsonrpc, in: container, debugDescription: "Invalid JSON-RPC version")
|
|
}
|
|
method = try container.decode(String.self, forKey: .method)
|
|
|
|
if N.Parameters.self is NotRequired.Type {
|
|
// For NotRequired parameters, use decodeIfPresent or init()
|
|
params =
|
|
(try container.decodeIfPresent(N.Parameters.self, forKey: .params)
|
|
?? (N.Parameters.self as! NotRequired.Type).init() as! N.Parameters)
|
|
} else if let value = try? container.decode(N.Parameters.self, forKey: .params) {
|
|
// If params exists and can be decoded, use it
|
|
params = value
|
|
} else if !container.contains(.params)
|
|
|| (try? container.decodeNil(forKey: .params)) == true
|
|
{
|
|
// If params is missing or explicitly null, use Empty for Empty parameters
|
|
// or throw for non-Empty parameters
|
|
if N.Parameters.self == Empty.self {
|
|
params = Empty() as! N.Parameters
|
|
} else {
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: container.codingPath,
|
|
debugDescription: "Missing required params field"))
|
|
}
|
|
} else {
|
|
throw DecodingError.dataCorrupted(
|
|
DecodingError.Context(
|
|
codingPath: container.codingPath,
|
|
debugDescription: "Invalid params field"))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A type-erased message for message handling
|
|
typealias AnyMessage = Message<AnyNotification>
|
|
|
|
extension Notification where Parameters == Empty {
|
|
/// Create a message with empty parameters.
|
|
public static func message() -> Message<Self> {
|
|
Message(method: name, params: Empty())
|
|
}
|
|
}
|
|
|
|
extension Notification {
|
|
/// Create a message with the given parameters.
|
|
public static func message(_ parameters: Parameters) -> Message<Self> {
|
|
Message(method: name, params: parameters)
|
|
}
|
|
}
|
|
|
|
/// A box for notification handlers that can be type-erased
|
|
class NotificationHandlerBox: @unchecked Sendable {
|
|
func callAsFunction(_ notification: Message<AnyNotification>) async throws {}
|
|
}
|
|
|
|
/// A typed notification handler that can be used to handle notifications of a specific type
|
|
final class TypedNotificationHandler<N: Notification>: NotificationHandlerBox,
|
|
@unchecked Sendable
|
|
{
|
|
private let _handle: @Sendable (Message<N>) async throws -> Void
|
|
|
|
init(_ handler: @escaping @Sendable (Message<N>) async throws -> Void) {
|
|
self._handle = handler
|
|
super.init()
|
|
}
|
|
|
|
override func callAsFunction(_ notification: Message<AnyNotification>) async throws {
|
|
// Create a concrete notification from the type-erased one
|
|
let data = try JSONEncoder().encode(notification)
|
|
let typedNotification = try JSONDecoder().decode(Message<N>.self, from: data)
|
|
|
|
try await _handle(typedNotification)
|
|
}
|
|
}
|