Initial implementation
This commit is contained in:
parent
cc603d35fe
commit
be1e9583f8
8 changed files with 4304 additions and 884 deletions
212
Sources/MCP/Base/Error.swift
Normal file
212
Sources/MCP/Base/Error.swift
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import Foundation
|
||||
|
||||
/// A model context protocol error.
|
||||
public enum Error: Sendable {
|
||||
// Standard JSON-RPC 2.0 errors (-32700 to -32603)
|
||||
case parseError(String?) // -32700
|
||||
case invalidRequest(String?) // -32600
|
||||
case methodNotFound(String?) // -32601
|
||||
case invalidParams(String?) // -32602
|
||||
case internalError(String?) // -32603
|
||||
|
||||
// Server errors (-32000 to -32099)
|
||||
case serverError(code: Int, message: String)
|
||||
|
||||
// Transport specific errors
|
||||
case connectionClosed
|
||||
case transportError(Swift.Error)
|
||||
|
||||
/// The JSON-RPC 2.0 error code
|
||||
public var code: Int {
|
||||
switch self {
|
||||
case .parseError: return -32700
|
||||
case .invalidRequest: return -32600
|
||||
case .methodNotFound: return -32601
|
||||
case .invalidParams: return -32602
|
||||
case .internalError: return -32603
|
||||
case .serverError(let code, _): return code
|
||||
case .connectionClosed: return -32000
|
||||
case .transportError: return -32001
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: LocalizedError
|
||||
|
||||
extension Error: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .parseError(let detail):
|
||||
return "Parse error: Invalid JSON" + (detail.map { ": \($0)" } ?? "")
|
||||
case .invalidRequest(let detail):
|
||||
return "Invalid Request" + (detail.map { ": \($0)" } ?? "")
|
||||
case .methodNotFound(let detail):
|
||||
return "Method not found" + (detail.map { ": \($0)" } ?? "")
|
||||
case .invalidParams(let detail):
|
||||
return "Invalid params" + (detail.map { ": \($0)" } ?? "")
|
||||
case .internalError(let detail):
|
||||
return "Internal error" + (detail.map { ": \($0)" } ?? "")
|
||||
case .serverError(_, let message):
|
||||
return "Server error: \(message)"
|
||||
case .connectionClosed:
|
||||
return "Connection closed"
|
||||
case .transportError(let error):
|
||||
return "Transport error: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
public var failureReason: String? {
|
||||
switch self {
|
||||
case .parseError:
|
||||
return "The server received invalid JSON that could not be parsed"
|
||||
case .invalidRequest:
|
||||
return "The JSON sent is not a valid Request object"
|
||||
case .methodNotFound:
|
||||
return "The method does not exist or is not available"
|
||||
case .invalidParams:
|
||||
return "Invalid method parameter(s)"
|
||||
case .internalError:
|
||||
return "Internal JSON-RPC error"
|
||||
case .serverError:
|
||||
return "Server-defined error occurred"
|
||||
case .connectionClosed:
|
||||
return "The connection to the server was closed"
|
||||
case .transportError(let error):
|
||||
return (error as? LocalizedError)?.failureReason ?? error.localizedDescription
|
||||
}
|
||||
}
|
||||
|
||||
public var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .parseError:
|
||||
return "Verify that the JSON being sent is valid and well-formed"
|
||||
case .invalidRequest:
|
||||
return "Ensure the request follows the JSON-RPC 2.0 specification format"
|
||||
case .methodNotFound:
|
||||
return "Check the method name and ensure it is supported by the server"
|
||||
case .invalidParams:
|
||||
return "Verify the parameters match the method's expected parameters"
|
||||
case .connectionClosed:
|
||||
return "Try reconnecting to the server"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CustomDebugStringConvertible
|
||||
|
||||
extension Error: CustomDebugStringConvertible {
|
||||
public var debugDescription: String {
|
||||
switch self {
|
||||
case .transportError(let error):
|
||||
return
|
||||
"[\(code)] \(errorDescription ?? "") (Underlying error: \(String(reflecting: error)))"
|
||||
default:
|
||||
return "[\(code)] \(errorDescription ?? "")"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
|
||||
extension Error: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case code, message, data
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(code, forKey: .code)
|
||||
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
|
||||
|
||||
// Encode additional data if available
|
||||
switch self {
|
||||
case .parseError(let detail),
|
||||
.invalidRequest(let detail),
|
||||
.methodNotFound(let detail),
|
||||
.invalidParams(let detail),
|
||||
.internalError(let detail):
|
||||
if let detail = detail {
|
||||
try container.encode(["detail": detail], forKey: .data)
|
||||
}
|
||||
case .serverError(_, _):
|
||||
// No additional data for server errors
|
||||
break
|
||||
case .connectionClosed:
|
||||
break
|
||||
case .transportError(let error):
|
||||
try container.encode(
|
||||
["error": error.localizedDescription],
|
||||
forKey: .data
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let code = try container.decode(Int.self, forKey: .code)
|
||||
let message = try container.decode(String.self, forKey: .message)
|
||||
let data = try container.decodeIfPresent([String: Value].self, forKey: .data)
|
||||
|
||||
switch code {
|
||||
case -32700:
|
||||
self = .parseError(data?["detail"] as? String ?? message)
|
||||
case -32600:
|
||||
self = .invalidRequest(data?["detail"] as? String ?? message)
|
||||
case -32601:
|
||||
self = .methodNotFound(data?["detail"] as? String ?? message)
|
||||
case -32602:
|
||||
self = .invalidParams(data?["detail"] as? String ?? message)
|
||||
case -32603:
|
||||
self = .internalError(data?["detail"] as? String ?? message)
|
||||
case -32000:
|
||||
self = .connectionClosed
|
||||
case -32001:
|
||||
self = .transportError(
|
||||
NSError(
|
||||
domain: "org.jsonrpc.error",
|
||||
code: code,
|
||||
userInfo: [NSLocalizedDescriptionKey: message]
|
||||
)
|
||||
)
|
||||
default:
|
||||
self = .serverError(code: code, message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Equatable
|
||||
|
||||
extension Error: Equatable {
|
||||
public static func == (lhs: Error, rhs: Error) -> Bool {
|
||||
lhs.code == rhs.code
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Hashable
|
||||
|
||||
extension Error: Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(code)
|
||||
switch self {
|
||||
case .parseError(let detail):
|
||||
hasher.combine(detail)
|
||||
case .invalidRequest(let detail):
|
||||
hasher.combine(detail)
|
||||
case .methodNotFound(let detail):
|
||||
hasher.combine(detail)
|
||||
case .invalidParams(let detail):
|
||||
hasher.combine(detail)
|
||||
case .internalError(let detail):
|
||||
hasher.combine(detail)
|
||||
case .serverError(_, let message):
|
||||
hasher.combine(message)
|
||||
case .connectionClosed:
|
||||
break
|
||||
case .transportError(let error):
|
||||
hasher.combine(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
69
Sources/MCP/Base/ID.swift
Normal file
69
Sources/MCP/Base/ID.swift
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import struct Foundation.UUID
|
||||
|
||||
/// A unique identifier for a request.
|
||||
public enum ID: Hashable, Sendable {
|
||||
/// A string ID.
|
||||
case string(String)
|
||||
|
||||
/// A number ID.
|
||||
case number(Int)
|
||||
|
||||
/// Generates a random string ID.
|
||||
public static var random: ID {
|
||||
return .string(UUID().uuidString)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByStringLiteral
|
||||
|
||||
extension ID: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String) {
|
||||
self = .string(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByIntegerLiteral
|
||||
|
||||
extension ID: ExpressibleByIntegerLiteral {
|
||||
public init(integerLiteral value: Int) {
|
||||
self = .number(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension ID: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .string(let str): return str
|
||||
case .number(let num): return String(num)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
extension ID: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
if let string = try? container.decode(String.self) {
|
||||
self = .string(string)
|
||||
} else if let number = try? container.decode(Int.self) {
|
||||
self = .number(number)
|
||||
} else if container.decodeNil() {
|
||||
// Handle unspecified/null IDs as empty string
|
||||
self = .string("")
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
in: container, debugDescription: "ID must be string or number")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .string(let str): try container.encode(str)
|
||||
case .number(let num): try container.encode(num)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
Sources/MCP/Base/Lifecycle.swift
Normal file
56
Sources/MCP/Base/Lifecycle.swift
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/// The initialization phase MUST be the first interaction between client and server.
|
||||
/// During this phase, the client and server:
|
||||
/// - Establish protocol version compatibility
|
||||
/// - Exchange and negotiate capabilities
|
||||
/// - Share implementation details
|
||||
///
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
|
||||
public enum Initialize: Method {
|
||||
public static let name: String = "initialize"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
public let protocolVersion: String
|
||||
public let capabilities: Client.Capabilities
|
||||
public let clientInfo: Client.Info
|
||||
|
||||
public init(
|
||||
protocolVersion: String = Version.latest,
|
||||
capabilities: Client.Capabilities,
|
||||
clientInfo: Client.Info
|
||||
) {
|
||||
self.protocolVersion = protocolVersion
|
||||
self.capabilities = capabilities
|
||||
self.clientInfo = clientInfo
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case protocolVersion, capabilities, clientInfo
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
protocolVersion =
|
||||
try container.decodeIfPresent(String.self, forKey: .protocolVersion)
|
||||
?? Version.latest
|
||||
capabilities =
|
||||
try container.decodeIfPresent(Client.Capabilities.self, forKey: .capabilities)
|
||||
?? .init()
|
||||
clientInfo =
|
||||
try container.decodeIfPresent(Client.Info.self, forKey: .clientInfo)
|
||||
?? .init(name: "unknown", version: "0.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let protocolVersion: String
|
||||
public let capabilities: Server.Capabilities
|
||||
public let serverInfo: Server.Info
|
||||
public let instructions: String?
|
||||
}
|
||||
}
|
||||
|
||||
/// After successful initialization, the client MUST send an initialized notification to indicate it is ready to begin normal operations.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/lifecycle/#initialization
|
||||
public struct InitializedNotification: Notification {
|
||||
public static let name: String = "notifications/initialized"
|
||||
}
|
||||
332
Sources/MCP/Base/Messages.swift
Normal file
332
Sources/MCP/Base/Messages.swift
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
import Foundation
|
||||
|
||||
private let jsonrpc = "2.0"
|
||||
|
||||
public struct Empty: Hashable, Codable, Sendable {}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// A method that can be used to send requests and receive responses.
|
||||
public protocol Method {
|
||||
/// 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 {
|
||||
static var name: String { "" }
|
||||
typealias Parameters = Value
|
||||
typealias Result = Value
|
||||
}
|
||||
|
||||
extension Method where Parameters == Empty {
|
||||
public static func request(id: ID = .random) -> Request<Self> {
|
||||
Request(id: id, method: name, params: Empty())
|
||||
}
|
||||
}
|
||||
|
||||
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) -> Request<Self> {
|
||||
Request(id: id, method: name, params: parameters)
|
||||
}
|
||||
|
||||
/// Create a response with the given result.
|
||||
public static func response(id: ID, result: Self.Result) -> Response<Self> {
|
||||
Response(id: id, result: result)
|
||||
}
|
||||
|
||||
/// Create a response with the given error.
|
||||
public static func response(id: ID, error: Error) -> Response<Self> {
|
||||
Response(id: id, error: error)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
init(id: ID = .random, method: String, params: M.Parameters) {
|
||||
self.id = id
|
||||
self.method = method
|
||||
self.params = params
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case jsonrpc, id, 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(id, forKey: .id)
|
||||
try container.encode(method, forKey: .method)
|
||||
if M.Parameters.self != Empty.self {
|
||||
try container.encode(params, forKey: .params)
|
||||
} else {
|
||||
// Encode empty object for Empty parameters
|
||||
try container.encode(Empty(), 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")
|
||||
}
|
||||
id = try container.decode(ID.self, forKey: .id)
|
||||
method = try container.decode(String.self, forKey: .method)
|
||||
if M.Parameters.self == Empty.self {
|
||||
if (try? container.decodeNil(forKey: .params)) != nil {
|
||||
params = Empty() as! M.Parameters
|
||||
} else if (try? container.decode(Empty.self, forKey: .params)) != nil {
|
||||
params = Empty() as! M.Parameters
|
||||
} else {
|
||||
// If params field is missing, use Empty
|
||||
params = Empty() as! M.Parameters
|
||||
}
|
||||
} else {
|
||||
params = try container.decode(M.Parameters.self, forKey: .params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased request for request/response handling
|
||||
typealias AnyRequest = Request<AnyMethod>
|
||||
|
||||
/// A box for request handlers that can be type-erased
|
||||
class RequestHandlerBox: @unchecked Sendable {
|
||||
func callAsFunction(_ request: Request<AnyMethod>) async throws -> Response<AnyMethod> {
|
||||
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: Request<AnyMethod>) async throws -> Response<AnyMethod>
|
||||
{
|
||||
// Create a concrete request from the type-erased one
|
||||
let data = try JSONEncoder().encode(request.params)
|
||||
let params = try JSONDecoder().decode(M.Parameters.self, from: data)
|
||||
let typedRequest = Request<M>(id: request.id, method: M.name, params: params)
|
||||
|
||||
// Handle with concrete type
|
||||
let response = try await _handle(typedRequest)
|
||||
|
||||
// Convert result to AnyMethod response
|
||||
switch response.result {
|
||||
case .success(let result):
|
||||
let resultData = try JSONEncoder().encode(result)
|
||||
let resultValue = try JSONDecoder().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, Error>
|
||||
|
||||
public init(id: ID, result: M.Result) {
|
||||
self.id = id
|
||||
self.result = .success(result)
|
||||
}
|
||||
|
||||
public init(id: ID, error: Error) {
|
||||
self.id = id
|
||||
self.result = .failure(error)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case jsonrpc, id, result, error
|
||||
}
|
||||
|
||||
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)
|
||||
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(Error.self, forKey: .error) {
|
||||
self.result = .failure(error)
|
||||
} else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: container.codingPath,
|
||||
debugDescription: "Invalid response"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A type-erased response for request/response handling
|
||||
typealias AnyResponse = Response<AnyMethod>
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// A notification message.
|
||||
public protocol Notification {
|
||||
/// 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 {
|
||||
static var name: String { "" }
|
||||
typealias Parameters = Empty
|
||||
}
|
||||
|
||||
/// 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)
|
||||
|
||||
// Handle params field being optional
|
||||
if N.Parameters.self == Empty.self {
|
||||
// For Empty parameters, use Empty() if params is missing or null
|
||||
params = Empty() 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)
|
||||
}
|
||||
}
|
||||
172
Sources/MCP/Base/Transports.swift
Normal file
172
Sources/MCP/Base/Transports.swift
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
import Darwin
|
||||
import Logging
|
||||
import SystemPackage
|
||||
|
||||
import struct Foundation.Data
|
||||
import struct Foundation.Date
|
||||
import class Foundation.JSONDecoder
|
||||
import class Foundation.JSONEncoder
|
||||
|
||||
/// Protocol defining the transport layer for MCP communication
|
||||
public protocol Transport: Actor {
|
||||
var logger: Logger { get }
|
||||
|
||||
/// Establishes connection with the transport
|
||||
func connect() async throws
|
||||
|
||||
/// Disconnects from the transport
|
||||
func disconnect() async
|
||||
|
||||
/// Sends a message string
|
||||
func send(_ message: String) async throws
|
||||
|
||||
/// Receives message strings as an async sequence
|
||||
func receive() -> AsyncThrowingStream<String, Swift.Error>
|
||||
}
|
||||
|
||||
/// Standard input/output transport implementation
|
||||
public actor StdioTransport: Transport {
|
||||
private let input: FileDescriptor
|
||||
private let output: FileDescriptor
|
||||
public nonisolated let logger: Logger
|
||||
|
||||
private var isConnected = false
|
||||
private let messageStream: AsyncStream<String>
|
||||
private let messageContinuation: AsyncStream<String>.Continuation
|
||||
|
||||
public init(
|
||||
input: FileDescriptor = FileDescriptor.standardInput,
|
||||
output: FileDescriptor = FileDescriptor.standardOutput,
|
||||
logger: Logger? = nil
|
||||
) {
|
||||
self.input = input
|
||||
self.output = output
|
||||
self.logger =
|
||||
logger
|
||||
?? Logger(
|
||||
label: "mcp.transport.stdio",
|
||||
factory: { _ in SwiftLogNoOpLogHandler() })
|
||||
|
||||
// Create message stream
|
||||
var continuation: AsyncStream<String>.Continuation!
|
||||
self.messageStream = AsyncStream { continuation = $0 }
|
||||
self.messageContinuation = continuation
|
||||
}
|
||||
|
||||
public func connect() async throws {
|
||||
guard !isConnected else { return }
|
||||
|
||||
// Set non-blocking mode
|
||||
try setNonBlocking(fileDescriptor: input)
|
||||
try setNonBlocking(fileDescriptor: output)
|
||||
|
||||
isConnected = true
|
||||
logger.info("Transport connected successfully")
|
||||
|
||||
// Start reading loop in background
|
||||
Task {
|
||||
await readLoop()
|
||||
}
|
||||
}
|
||||
|
||||
private func setNonBlocking(fileDescriptor: FileDescriptor) throws {
|
||||
let flags = fcntl(fileDescriptor.rawValue, F_GETFL)
|
||||
guard flags >= 0 else {
|
||||
throw Error.transportError(Errno.badFileDescriptor)
|
||||
}
|
||||
let result = fcntl(fileDescriptor.rawValue, F_SETFL, flags | O_NONBLOCK)
|
||||
guard result >= 0 else {
|
||||
throw Error.transportError(Errno.badFileDescriptor)
|
||||
}
|
||||
}
|
||||
|
||||
private func readLoop() async {
|
||||
let bufferSize = 4096
|
||||
var buffer = [UInt8](repeating: 0, count: bufferSize)
|
||||
var pendingData = Data()
|
||||
|
||||
while isConnected && !Task.isCancelled {
|
||||
do {
|
||||
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
|
||||
try input.read(into: UnsafeMutableRawBufferPointer(pointer))
|
||||
}
|
||||
|
||||
if bytesRead == 0 {
|
||||
logger.notice("EOF received")
|
||||
break
|
||||
}
|
||||
|
||||
pendingData.append(Data(buffer[..<bytesRead]))
|
||||
|
||||
// Process complete messages
|
||||
while let newlineIndex = pendingData.firstIndex(of: UInt8(ascii: "\n")) {
|
||||
let messageData = pendingData[..<newlineIndex]
|
||||
pendingData = pendingData[(newlineIndex + 1)...]
|
||||
|
||||
if let message = String(data: messageData, encoding: .utf8),
|
||||
!message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
{
|
||||
logger.debug("Message received", metadata: ["message": "\(message)"])
|
||||
messageContinuation.yield(message)
|
||||
}
|
||||
}
|
||||
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
|
||||
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms backoff
|
||||
continue
|
||||
} catch {
|
||||
if !Task.isCancelled {
|
||||
logger.error("Read error occurred", metadata: ["error": "\(error)"])
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
messageContinuation.finish()
|
||||
}
|
||||
|
||||
public func disconnect() async {
|
||||
guard isConnected else { return }
|
||||
isConnected = false
|
||||
messageContinuation.finish()
|
||||
logger.info("Transport disconnected")
|
||||
}
|
||||
|
||||
public func send(_ message: String) async throws {
|
||||
guard isConnected else {
|
||||
throw Error.transportError(Errno.socketNotConnected)
|
||||
}
|
||||
|
||||
let message = message + "\n"
|
||||
guard let data = message.data(using: .utf8) else {
|
||||
throw Error.transportError(Errno.invalidArgument)
|
||||
}
|
||||
|
||||
var remaining = data
|
||||
while !remaining.isEmpty {
|
||||
do {
|
||||
let written = try remaining.withUnsafeBytes { buffer in
|
||||
try output.write(UnsafeRawBufferPointer(buffer))
|
||||
}
|
||||
if written > 0 {
|
||||
remaining = remaining.dropFirst(written)
|
||||
}
|
||||
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
|
||||
try await Task.sleep(nanoseconds: 10_000_000) // 10ms backoff
|
||||
continue
|
||||
} catch {
|
||||
throw Error.transportError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func receive() -> AsyncThrowingStream<String, Swift.Error> {
|
||||
return AsyncThrowingStream { continuation in
|
||||
Task {
|
||||
for await message in messageStream {
|
||||
continuation.yield(message)
|
||||
}
|
||||
continuation.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
5
Sources/MCP/Base/Utilities/Ping.swift
Normal file
5
Sources/MCP/Base/Utilities/Ping.swift
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
/// The Model Context Protocol includes an optional ping mechanism that allows either party to verify that their counterpart is still responsive and the connection is alive.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/utilities/ping
|
||||
public enum Ping: Method {
|
||||
public static let name: String = "ping"
|
||||
}
|
||||
402
Sources/MCP/Base/Value.swift
Normal file
402
Sources/MCP/Base/Value.swift
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
import struct Foundation.Data
|
||||
import class Foundation.JSONDecoder
|
||||
import class Foundation.JSONEncoder
|
||||
|
||||
/// A codable value.
|
||||
public enum Value: Hashable, Sendable {
|
||||
case null
|
||||
case bool(Bool)
|
||||
case int(Int)
|
||||
case double(Double)
|
||||
case string(String)
|
||||
case data(mimeType: String? = nil, Data)
|
||||
case array([Value])
|
||||
case object([String: Value])
|
||||
|
||||
/// Create a `Value` from a `Codable` value.
|
||||
/// - Parameter value: The codable value
|
||||
/// - Returns: A value
|
||||
public init<T: Codable>(_ value: T) throws {
|
||||
if let valueAsValue = value as? Value {
|
||||
self = valueAsValue
|
||||
} else {
|
||||
let data = try JSONEncoder().encode(value)
|
||||
self = try JSONDecoder().decode(Value.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the value is `null`.
|
||||
public var isNull: Bool {
|
||||
return self == .null
|
||||
}
|
||||
|
||||
/// Returns the `Bool` value if the value is a `bool`,
|
||||
/// otherwise returns `nil`.
|
||||
public var boolValue: Bool? {
|
||||
guard case let .bool(value) = self else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
/// Returns the `Int` value if the value is an `integer`,
|
||||
/// otherwise returns `nil`.
|
||||
public var intValue: Int? {
|
||||
guard case let .int(value) = self else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
/// Returns the `Double` value if the value is a `double`,
|
||||
/// otherwise returns `nil`.
|
||||
public var doubleValue: Double? {
|
||||
guard case let .double(value) = self else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
/// Returns the `String` value if the value is a `string`,
|
||||
/// otherwise returns `nil`.
|
||||
public var stringValue: String? {
|
||||
guard case let .string(value) = self else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
/// Returns the data value and optional MIME type if the value is `data`,
|
||||
/// otherwise returns `nil`.
|
||||
public var dataValue: (mimeType: String?, Data)? {
|
||||
guard case let .data(mimeType: mimeType, data) = self else { return nil }
|
||||
return (mimeType: mimeType, data)
|
||||
}
|
||||
|
||||
/// Returns the `[Value]` value if the value is an `array`,
|
||||
/// otherwise returns `nil`.
|
||||
public var arrayValue: [Value]? {
|
||||
guard case let .array(value) = self else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
/// Returns the `[String: Value]` value if the value is an `object`,
|
||||
/// otherwise returns `nil`.
|
||||
public var objectValue: [String: Value]? {
|
||||
guard case let .object(value) = self else { return nil }
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
extension Value: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if container.decodeNil() {
|
||||
self = .null
|
||||
} else if let value = try? container.decode(Bool.self) {
|
||||
self = .bool(value)
|
||||
} else if let value = try? container.decode(Int.self) {
|
||||
self = .int(value)
|
||||
} else if let value = try? container.decode(Double.self) {
|
||||
self = .double(value)
|
||||
} else if let value = try? container.decode(String.self) {
|
||||
if Data.isDataURL(string: value),
|
||||
case let (mimeType, data)? = Data.parseDataURL(value)
|
||||
{
|
||||
self = .data(mimeType: mimeType, data)
|
||||
} else {
|
||||
self = .string(value)
|
||||
}
|
||||
} else if let value = try? container.decode([Value].self) {
|
||||
self = .array(value)
|
||||
} else if let value = try? container.decode([String: Value].self) {
|
||||
self = .object(value)
|
||||
} else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
in: container, debugDescription: "Value type not found")
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .null:
|
||||
try container.encodeNil()
|
||||
case .bool(let value):
|
||||
try container.encode(value)
|
||||
case .int(let value):
|
||||
try container.encode(value)
|
||||
case .double(let value):
|
||||
try container.encode(value)
|
||||
case .string(let value):
|
||||
try container.encode(value)
|
||||
case let .data(mimeType, value):
|
||||
try container.encode(value.dataURLEncoded(mimeType: mimeType))
|
||||
case .array(let value):
|
||||
try container.encode(value)
|
||||
case .object(let value):
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Value: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .null:
|
||||
return ""
|
||||
case .bool(let value):
|
||||
return value.description
|
||||
case .int(let value):
|
||||
return value.description
|
||||
case .double(let value):
|
||||
return value.description
|
||||
case .string(let value):
|
||||
return value.description
|
||||
case let .data(mimeType, value):
|
||||
return value.dataURLEncoded(mimeType: mimeType)
|
||||
case .array(let value):
|
||||
return value.description
|
||||
case .object(let value):
|
||||
return value.description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByNilLiteral
|
||||
|
||||
extension Value: ExpressibleByNilLiteral {
|
||||
public init(nilLiteral: ()) {
|
||||
self = .null
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByBooleanLiteral
|
||||
|
||||
extension Value: ExpressibleByBooleanLiteral {
|
||||
public init(booleanLiteral value: Bool) {
|
||||
self = .bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByIntegerLiteral
|
||||
|
||||
extension Value: ExpressibleByIntegerLiteral {
|
||||
public init(integerLiteral value: Int) {
|
||||
self = .int(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByFloatLiteral
|
||||
|
||||
extension Value: ExpressibleByFloatLiteral {
|
||||
public init(floatLiteral value: Double) {
|
||||
self = .double(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByStringLiteral
|
||||
|
||||
extension Value: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String) {
|
||||
self = .string(value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByArrayLiteral
|
||||
|
||||
extension Value: ExpressibleByArrayLiteral {
|
||||
public init(arrayLiteral elements: Value...) {
|
||||
self = .array(elements)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByDictionaryLiteral
|
||||
|
||||
extension Value: ExpressibleByDictionaryLiteral {
|
||||
public init(dictionaryLiteral elements: (String, Value)...) {
|
||||
var dictionary: [String: Value] = [:]
|
||||
for (key, value) in elements {
|
||||
dictionary[key] = value
|
||||
}
|
||||
self = .object(dictionary)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ExpressibleByStringInterpolation
|
||||
|
||||
extension Value: ExpressibleByStringInterpolation {
|
||||
public struct StringInterpolation: StringInterpolationProtocol {
|
||||
var stringValue: String
|
||||
|
||||
public init(literalCapacity: Int, interpolationCount: Int) {
|
||||
self.stringValue = ""
|
||||
self.stringValue.reserveCapacity(literalCapacity + interpolationCount)
|
||||
}
|
||||
|
||||
public mutating func appendLiteral(_ literal: String) {
|
||||
self.stringValue.append(literal)
|
||||
}
|
||||
|
||||
public mutating func appendInterpolation<T: CustomStringConvertible>(_ value: T) {
|
||||
self.stringValue.append(value.description)
|
||||
}
|
||||
}
|
||||
|
||||
public init(stringInterpolation: StringInterpolation) {
|
||||
self = .string(stringInterpolation.stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Standard Library Type Extensions
|
||||
|
||||
extension Bool {
|
||||
/// Creates a boolean value from a `Value` instance.
|
||||
///
|
||||
/// In strict mode, only `.bool` values are converted. In non-strict mode, the following conversions are supported:
|
||||
/// - Integers: `1` is `true`, `0` is `false`
|
||||
/// - Doubles: `1.0` is `true`, `0.0` is `false`
|
||||
/// - Strings (lowercase only):
|
||||
/// - `true`: "true", "t", "yes", "y", "on", "1"
|
||||
/// - `false`: "false", "f", "no", "n", "off", "0"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The `Value` to convert
|
||||
/// - strict: When `true`, only converts from `.bool` values. Defaults to `true`
|
||||
/// - Returns: A boolean value if conversion is possible, `nil` otherwise
|
||||
///
|
||||
/// - Example:
|
||||
/// ```swift
|
||||
/// Bool(Value.bool(true)) // Returns true
|
||||
/// Bool(Value.int(1), strict: false) // Returns true
|
||||
/// Bool(Value.string("yes"), strict: false) // Returns true
|
||||
/// ```
|
||||
public init?(_ value: Value, strict: Bool = true) {
|
||||
switch value {
|
||||
case .bool(let b):
|
||||
self = b
|
||||
case .int(let i) where !strict:
|
||||
switch i {
|
||||
case 0: self = false
|
||||
case 1: self = true
|
||||
default: return nil
|
||||
}
|
||||
case .double(let d) where !strict:
|
||||
switch d {
|
||||
case 0.0: self = false
|
||||
case 1.0: self = true
|
||||
default: return nil
|
||||
}
|
||||
case .string(let s) where !strict:
|
||||
switch s {
|
||||
case "true", "t", "yes", "y", "on", "1":
|
||||
self = true
|
||||
case "false", "f", "no", "n", "off", "0":
|
||||
self = false
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Int {
|
||||
/// Creates an integer value from a `Value` instance.
|
||||
///
|
||||
/// In strict mode, only `.int` values are converted. In non-strict mode, the following conversions are supported:
|
||||
/// - Doubles: Converted if they can be represented exactly as integers
|
||||
/// - Strings: Parsed if they contain a valid integer representation
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The `Value` to convert
|
||||
/// - strict: When `true`, only converts from `.int` values. Defaults to `true`
|
||||
/// - Returns: An integer value if conversion is possible, `nil` otherwise
|
||||
///
|
||||
/// - Example:
|
||||
/// ```swift
|
||||
/// Int(Value.int(42)) // Returns 42
|
||||
/// Int(Value.double(42.0), strict: false) // Returns 42
|
||||
/// Int(Value.string("42"), strict: false) // Returns 42
|
||||
/// Int(Value.double(42.5), strict: false) // Returns nil
|
||||
/// ```
|
||||
public init?(_ value: Value, strict: Bool = true) {
|
||||
switch value {
|
||||
case .int(let i):
|
||||
self = i
|
||||
case .double(let d) where !strict:
|
||||
guard let intValue = Int(exactly: d) else { return nil }
|
||||
self = intValue
|
||||
case .string(let s) where !strict:
|
||||
guard let intValue = Int(s) else { return nil }
|
||||
self = intValue
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Double {
|
||||
/// Creates a double value from a `Value` instance.
|
||||
///
|
||||
/// In strict mode, converts from `.double` and `.int` values. In non-strict mode, the following conversions are supported:
|
||||
/// - Integers: Converted to their double representation
|
||||
/// - Strings: Parsed if they contain a valid floating-point representation
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The `Value` to convert
|
||||
/// - strict: When `true`, only converts from `.double` and `.int` values. Defaults to `true`
|
||||
/// - Returns: A double value if conversion is possible, `nil` otherwise
|
||||
///
|
||||
/// - Example:
|
||||
/// ```swift
|
||||
/// Double(Value.double(42.5)) // Returns 42.5
|
||||
/// Double(Value.int(42)) // Returns 42.0
|
||||
/// Double(Value.string("42.5"), strict: false) // Returns 42.5
|
||||
/// ```
|
||||
public init?(_ value: Value, strict: Bool = true) {
|
||||
switch value {
|
||||
case .double(let d):
|
||||
self = d
|
||||
case .int(let i):
|
||||
self = Double(i)
|
||||
case .string(let s) where !strict:
|
||||
guard let doubleValue = Double(s) else { return nil }
|
||||
self = doubleValue
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// Creates a string value from a `Value` instance.
|
||||
///
|
||||
/// In strict mode, only `.string` values are converted. In non-strict mode, the following conversions are supported:
|
||||
/// - Integers: Converted to their string representation
|
||||
/// - Doubles: Converted to their string representation
|
||||
/// - Booleans: Converted to "true" or "false"
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The `Value` to convert
|
||||
/// - strict: When `true`, only converts from `.string` values. Defaults to `true`
|
||||
/// - Returns: A string value if conversion is possible, `nil` otherwise
|
||||
///
|
||||
/// - Example:
|
||||
/// ```swift
|
||||
/// String(Value.string("hello")) // Returns "hello"
|
||||
/// String(Value.int(42), strict: false) // Returns "42"
|
||||
/// String(Value.bool(true), strict: false) // Returns "true"
|
||||
/// ```
|
||||
public init?(_ value: Value, strict: Bool = true) {
|
||||
switch value {
|
||||
case .string(let s):
|
||||
self = s
|
||||
case .int(let i) where !strict:
|
||||
self = String(i)
|
||||
case .double(let d) where !strict:
|
||||
self = String(d)
|
||||
case .bool(let b) where !strict:
|
||||
self = String(b)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Sources/MCP/Base/Versioning.swift
Normal file
11
Sources/MCP/Base/Versioning.swift
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import Foundation
|
||||
|
||||
/// The Model Context Protocol uses string-based version identifiers
|
||||
/// following the format YYYY-MM-DD, to indicate
|
||||
/// the last date backwards incompatible changes were made.
|
||||
///
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/basic/versioning/
|
||||
public enum Version {
|
||||
/// The current protocol version.
|
||||
public static let latest = "2024-11-05"
|
||||
}
|
||||
Loading…
Reference in a new issue