Initial implementation

This commit is contained in:
Mattt Zmuda 2025-02-11 09:27:37 -08:00
commit be1e9583f8
No known key found for this signature in database
18 changed files with 4304 additions and 884 deletions

View 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
View 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)
}
}
}

View 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"
}

View 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)
}
}

View 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()
}
}
}
}

View 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"
}

View 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
}
}
}

View 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"
}

View file

@ -0,0 +1,377 @@
import Logging
import SystemPackage
import struct Foundation.Data
import struct Foundation.Date
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// Model Context Protocol client
public actor Client {
/// Implementation information
public struct Info: Hashable, Codable, Sendable {
/// The client name
public var name: String
/// The client version
public var version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// The client capabilities
public struct Capabilities: Hashable, Codable, Sendable {
/// The roots capabilities
public struct Roots: Hashable, Codable, Sendable {
/// Whether the list of roots has changed
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// The sampling capabilities
public struct Sampling: Hashable, Codable, Sendable {
public init() {}
}
/// Whether the client supports sampling
public var sampling: Sampling?
/// Experimental features supported by the client
public var experimental: [String: String]?
/// Whether the client supports roots
public var roots: Capabilities.Roots?
public init(
sampling: Sampling? = nil,
experimental: [String: String]? = nil,
roots: Capabilities.Roots? = nil
) {
self.sampling = sampling
self.experimental = experimental
self.roots = roots
}
}
/// The connection to the server
private var connection: (any Transport)?
/// The logger for the client
private var logger: Logger? {
get async {
await connection?.logger
}
}
/// The client information
private let clientInfo: Client.Info
/// The client name
public nonisolated var name: String { clientInfo.name }
/// The client version
public nonisolated var version: String { clientInfo.version }
/// The client capabilities
public var capabilities: Client.Capabilities
/// The server capabilities
private var serverCapabilities: Server.Capabilities?
/// The server version
private var serverVersion: String?
/// The server instructions
private var instructions: String?
/// A dictionary of type-erased notification handlers, keyed by method name
private var notificationHandlers: [String: [NotificationHandlerBox]] = [:]
/// The task for the message handling loop
private var task: Task<Void, Never>?
/// A pending request with a continuation for the result
private struct PendingRequest<T> {
let continuation: CheckedContinuation<T, Swift.Error>
}
/// A dictionary of type-erased pending requests, keyed by request ID
private var pendingRequests: [ID: Any] = [:]
public init(
name: String,
version: String
) {
self.clientInfo = Client.Info(name: name, version: version)
self.capabilities = Capabilities()
}
/// Connect to the server using the given transport
public func connect(transport: any Transport) async throws {
self.connection = transport
try await self.connection?.connect()
await logger?.info(
"Client connected", metadata: ["name": "\(name)", "version": "\(version)"])
// Start message handling loop
task = Task {
guard let connection = self.connection else { return }
repeat {
// Check for cancellation before starting the iteration
if Task.isCancelled { break }
do {
let stream = await connection.receive()
for try await string in stream {
if Task.isCancelled { break } // Check inside loop too
// Decode and handle incoming message
guard let data = string.data(using: .utf8) else {
throw Error.parseError("Invalid UTF-8 data")
}
// Attempt to decode string data as AnyResponse or AnyMessage
let decoder = JSONDecoder()
if let response = try? decoder.decode(AnyResponse.self, from: data) {
await handleResponse(response, for: response)
} else if let message = try? decoder.decode(AnyMessage.self, from: data) {
await handleMessage(message)
} else {
var metadata: Logger.Metadata = [:]
if let string = String(data: data, encoding: .utf8) {
metadata["message"] = .string(string)
}
await logger?.warning(
"Unexpected message received by client", metadata: metadata)
}
}
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
continue
} catch {
await logger?.error(
"Error in message handling loop", metadata: ["error": "\(error)"])
break
}
} while true
}
}
/// Disconnect the client and cancel all pending requests
public func disconnect() async {
// Cancel all pending requests
for (id, request) in pendingRequests {
// We know this cast is safe because we only store PendingRequest values
if let typedRequest = request as? PendingRequest<Any> {
typedRequest.continuation.resume(
throwing: Error.internalError("Client disconnected"))
}
pendingRequests.removeValue(forKey: id)
}
task?.cancel()
task = nil
if let connection = connection {
await connection.disconnect()
}
connection = nil
}
// MARK: - Registration
/// Register a handler for a notification
@discardableResult
public func onNotification<N: Notification>(
_ type: N.Type,
handler: @escaping @Sendable (Message<N>) async throws -> Void
) async -> Self {
let handlers = notificationHandlers[N.name, default: []]
notificationHandlers[N.name] = handlers + [TypedNotificationHandler(handler)]
return self
}
// MARK: - Requests
/// Send a request and receive its response
public func send<M: Method>(_ request: Request<M>) async throws -> M.Result {
guard let connection = connection else {
throw Error.internalError("Client connection not initialized")
}
let requestData = try JSONEncoder().encode(request)
guard let requestString = String(data: requestData, encoding: .utf8) else {
throw Error.internalError("Failed to encode request")
}
return try await withCheckedThrowingContinuation { continuation in
// Store the pending request first
Task {
self.addPendingRequest(
id: request.id,
continuation: continuation,
type: M.Result.self
)
// Send the request
do {
try await connection.send(requestString)
} catch {
continuation.resume(throwing: error)
self.removePendingRequest(id: request.id)
}
}
}
}
private func addPendingRequest<T>(
id: ID,
continuation: CheckedContinuation<T, Swift.Error>,
type: T.Type
) {
pendingRequests[id] = PendingRequest(continuation: continuation)
}
private func removePendingRequest(id: ID) {
pendingRequests.removeValue(forKey: id)
}
// MARK: - Lifecycle
public func initialize() async throws -> Initialize.Result {
let request = Initialize.request(
.init(
protocolVersion: Version.latest,
capabilities: capabilities,
clientInfo: clientInfo
))
let result = try await send(request)
self.serverCapabilities = result.capabilities
self.serverVersion = result.protocolVersion
self.instructions = result.instructions
return result
}
public func ping() async throws {
let request = Ping.request()
_ = try await send(request)
}
// MARK: - Prompts
public func getPrompt(name: String, arguments: [String: Value]? = nil) async throws
-> (description: String?, messages: [Prompt.Message])
{
_ = try checkCapability(\.prompts, "Prompts")
let request = GetPrompt.request(.init(name: name, arguments: arguments))
let result = try await send(request)
return (description: result.description, messages: result.messages)
}
public func listPrompts(cursor: String? = nil) async throws
-> (prompts: [Prompt], nextCursor: String?)
{
_ = try checkCapability(\.prompts, "Prompts")
let request = ListPrompts.request(.init(cursor: cursor))
let result = try await send(request)
return (prompts: result.prompts, nextCursor: result.nextCursor)
}
// MARK: - Resources
public func readResource(uri: String) async throws -> [Resource.Content] {
_ = try checkCapability(\.resources?.read, "Resource reading")
let request = ReadResource.request(.init(uri: uri))
let result = try await send(request)
return result.contents
}
public func listResources(cursor: String? = nil) async throws -> (
resources: [Resource], nextCursor: String?
) {
_ = try checkCapability(\.resources?.list, "Resource listing")
let request = ListResources.request(.init(cursor: cursor))
let result = try await send(request)
return (resources: result.resources, nextCursor: result.nextCursor)
}
public func subscribeToResource(uri: String) async throws {
_ = try checkCapability(\.resources?.subscribe, "Resource subscription")
let request = ResourceSubscribe.request(.init(uri: uri))
_ = try await send(request)
}
// MARK: - Tools
public func listTools(cursor: String? = nil) async throws -> [Tool] {
_ = try checkCapability(\.tools, "Tools")
let request = ListTools.request(.init(cursor: cursor))
let result = try await send(request)
return result.tools
}
public func callTool(name: String, arguments: [String: Value]? = nil) async throws -> (
content: [Tool.Content], isError: Bool
) {
_ = try checkCapability(\.tools, "Tools")
let request = CallTool.request(.init(name: name, arguments: arguments))
let result = try await send(request)
return (content: result.content, isError: result.isError)
}
// MARK: -
private func handleResponse(_ response: Response<AnyMethod>, for request: Any) async {
await logger?.debug(
"Processing response",
metadata: ["id": "\(response.id)"])
// We know this cast is safe because we only store PendingRequest values
guard let typedRequest = request as? PendingRequest<Any> else { return }
switch response.result {
case .success(let value):
typedRequest.continuation.resume(returning: value)
case .failure(let error):
typedRequest.continuation.resume(throwing: error)
}
removePendingRequest(id: response.id)
}
private func handleMessage(_ message: Message<AnyNotification>) async {
await logger?.debug(
"Processing notification",
metadata: ["method": "\(message.method)"])
// Find notification handlers for this method
guard let handlers = notificationHandlers[message.method] else { return }
// Convert notification parameters to concrete type and call handlers
for handler in handlers {
do {
try await handler(message)
} catch {
await logger?.error(
"Error handling notification",
metadata: [
"method": "\(message.method)",
"error": "\(error)",
])
}
}
}
// MARK: -
private func checkCapability<T>(_ keyPath: KeyPath<Server.Capabilities, T?>, _ name: String)
throws -> T
{
guard let capabilities = serverCapabilities else {
throw Error.methodNotFound("Server capabilities not initialized")
}
guard let value = capabilities[keyPath: keyPath] else {
throw Error.methodNotFound("\(name) is not supported by the server")
}
return value
}
}

View file

@ -0,0 +1,87 @@
import Foundation
import RegexBuilder
extension Data {
/// Regex pattern for data URLs
@inline(__always) private static var dataURLRegex:
Regex<(Substring, Substring, Substring?, Substring)>
{
Regex {
"data:"
Capture {
ZeroOrMore(.reluctant) {
CharacterClass.anyOf(",;").inverted
}
}
Optionally {
";charset="
Capture {
OneOrMore(.reluctant) {
CharacterClass.anyOf(",;").inverted
}
}
}
Optionally { ";base64" }
","
Capture {
ZeroOrMore { .any }
}
}
}
/// Checks if a given string is a valid data URL.
///
/// - Parameter string: The string to check.
/// - Returns: `true` if the string is a valid data URL, otherwise `false`.
/// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html)
public static func isDataURL(string: String) -> Bool {
return string.wholeMatch(of: dataURLRegex) != nil
}
/// Parses a data URL string into its MIME type and data components.
///
/// - Parameter string: The data URL string to parse.
/// - Returns: A tuple containing the MIME type and decoded data, or `nil` if parsing fails.
/// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html)
public static func parseDataURL(_ string: String) -> (mimeType: String, data: Data)? {
guard let match = string.wholeMatch(of: dataURLRegex) else {
return nil
}
// Extract components using strongly typed captures
let (_, mediatype, charset, encodedData) = match.output
let isBase64 = string.contains(";base64,")
// Process MIME type
var mimeType = mediatype.isEmpty ? "text/plain" : String(mediatype)
if let charset = charset, !charset.isEmpty, mimeType.starts(with: "text/") {
mimeType += ";charset=\(charset)"
}
// Decode data
let decodedData: Data
if isBase64 {
guard let base64Data = Data(base64Encoded: String(encodedData)) else { return nil }
decodedData = base64Data
} else {
guard
let percentDecodedData = String(encodedData).removingPercentEncoding?.data(
using: .utf8)
else { return nil }
decodedData = percentDecodedData
}
return (mimeType: mimeType, data: decodedData)
}
/// Encodes the data as a data URL string with an optional MIME type.
///
/// - Parameter mimeType: The MIME type of the data. If `nil`, "text/plain" will be used.
/// - Returns: A data URL string representation of the data.
/// - SeeAlso: [RFC 2397](https://www.rfc-editor.org/rfc/rfc2397.html)
public func dataURLEncoded(mimeType: String? = nil) -> String {
let base64Data = self.base64EncodedString()
return "data:\(mimeType ?? "text/plain");base64,\(base64Data)"
}
}

View file

@ -1,364 +0,0 @@
import Foundation
/// Protocol version constants
public enum MCPVersion {
public static let latest = "1.0.0"
public static let supported = ["1.0.0"]
}
/// MCP error types
public enum MCPError: LocalizedError {
case connectionClosed
case invalidResponse
case invalidParams(String)
case serverError(String)
case protocolError(String)
case transportError(Error)
public var errorDescription: String? {
switch self {
case .connectionClosed:
return "Connection closed"
case .invalidResponse:
return "Invalid response from server"
case .invalidParams(let message):
return "Invalid parameters: \(message)"
case .serverError(let message):
return "Server error: \(message)"
case .protocolError(let message):
return "Protocol error: \(message)"
case .transportError(let error):
return "Transport error: \(error.localizedDescription)"
}
}
}
/// Base protocol for MCP messages
public protocol MCPMessage: Codable {
var jsonrpc: String { get }
}
/// Protocol for MCP requests
public protocol MCPRequest: MCPMessage {
var id: String { get }
var method: String { get }
}
/// Protocol for MCP notifications
public protocol MCPNotification: MCPMessage {
var method: String { get }
}
/// Protocol for MCP responses
public protocol MCPResponse: MCPMessage {
var id: String { get }
}
/// Implementation information
public struct Implementation: Codable {
public let name: String
public let version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// Client capabilities
public struct ClientCapabilities: Codable {
public var sampling: Bool?
public var experimental: [String: [String: Any]]?
public var roots: RootsCapability?
public init(sampling: Bool? = nil,
experimental: [String: [String: Any]]? = nil,
roots: RootsCapability? = nil) {
self.sampling = sampling
self.experimental = experimental
self.roots = roots
}
enum CodingKeys: String, CodingKey {
case sampling
case experimental
case roots
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
sampling = try container.decodeIfPresent(Bool.self, forKey: .sampling)
experimental = try container.decodeIfPresent([String: [String: Any]].self, forKey: .experimental)
roots = try container.decodeIfPresent(RootsCapability.self, forKey: .roots)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(sampling, forKey: .sampling)
try container.encodeIfPresent(experimental, forKey: .experimental)
try container.encodeIfPresent(roots, forKey: .roots)
}
}
/// Roots capability configuration
public struct RootsCapability: Codable {
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Server capabilities
public struct ServerCapabilities: Codable {
public var logging: Bool?
public var prompts: Bool?
public var resources: ResourcesCapability?
public var tools: Bool?
public init(logging: Bool? = nil,
prompts: Bool? = nil,
resources: ResourcesCapability? = nil,
tools: Bool? = nil) {
self.logging = logging
self.prompts = prompts
self.resources = resources
self.tools = tools
}
}
/// Resources capability configuration
public struct ResourcesCapability: Codable {
public var subscribe: Bool?
public init(subscribe: Bool? = nil) {
self.subscribe = subscribe
}
}
/// Initialize request parameters
public struct InitializeRequestParams: Codable {
public let protocolVersion: String
public let capabilities: ClientCapabilities
public let clientInfo: Implementation
public init(protocolVersion: String = MCPVersion.latest,
capabilities: ClientCapabilities,
clientInfo: Implementation) {
self.protocolVersion = protocolVersion
self.capabilities = capabilities
self.clientInfo = clientInfo
}
}
/// Initialize request
public struct InitializeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "initialize"
public let params: InitializeRequestParams
public init(id: String = UUID().uuidString, params: InitializeRequestParams) {
self.id = id
self.params = params
}
}
/// Initialize result
public struct InitializeResult: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: InitializeResultData
public struct InitializeResultData: Codable {
public let protocolVersion: String
public let capabilities: ServerCapabilities
public let serverInfo: Implementation
public let instructions: String?
}
}
/// MCP Client protocol
public protocol MCPClientProtocol {
func connect() async throws
func disconnect() async
func initialize() async throws -> InitializeResult
func ping() async throws
// Resource methods
func listResources() async throws -> ListResourcesResponse
func readResource(_ uri: String) async throws -> ReadResourceResponse
func subscribeResource(_ uri: String) async throws
func unsubscribeResource(_ uri: String) async throws
// Tool methods
func listTools() async throws -> ListToolsResponse
func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse
}
/// MCP Client implementation
public actor MCPClient: MCPClientProtocol {
private let transport: MCPTransport
private let clientInfo: Implementation
private var capabilities: ClientCapabilities
private var serverCapabilities: ServerCapabilities?
private var serverVersion: Implementation?
private var instructions: String?
public init(transport: MCPTransport,
clientInfo: Implementation,
capabilities: ClientCapabilities = ClientCapabilities()) {
self.transport = transport
self.clientInfo = clientInfo
self.capabilities = capabilities
}
public func connect() async throws {
try await transport.connect()
}
public func disconnect() async {
await transport.disconnect()
}
public func initialize() async throws -> InitializeResult {
let request = InitializeRequest(params: InitializeRequestParams(
capabilities: capabilities,
clientInfo: clientInfo
))
let response: InitializeResult = try await sendRequest(request)
// Validate protocol version
guard MCPVersion.supported.contains(response.result.protocolVersion) else {
throw MCPError.protocolError("Unsupported protocol version: \(response.result.protocolVersion)")
}
// Store server information
serverCapabilities = response.result.capabilities
serverVersion = response.result.serverInfo
instructions = response.result.instructions
// Send initialized notification
try await sendNotification(InitializedNotification())
return response
}
public func ping() async throws {
let request = PingRequest()
try await sendRequest(request)
}
// MARK: - Resource Methods
public func listResources() async throws -> ListResourcesResponse {
guard serverCapabilities?.resources != nil else {
throw MCPError.protocolError("Server does not support resources")
}
let request = ListResourcesRequest()
return try await sendRequest(request)
}
public func readResource(_ uri: String) async throws -> ReadResourceResponse {
guard serverCapabilities?.resources != nil else {
throw MCPError.protocolError("Server does not support resources")
}
let request = ReadResourceRequest(uri: uri)
return try await sendRequest(request)
}
public func subscribeResource(_ uri: String) async throws {
guard let resources = serverCapabilities?.resources,
resources.subscribe == true else {
throw MCPError.protocolError("Server does not support resource subscriptions")
}
let request = SubscribeRequest(uri: uri)
try await sendRequest(request)
}
public func unsubscribeResource(_ uri: String) async throws {
guard let resources = serverCapabilities?.resources,
resources.subscribe == true else {
throw MCPError.protocolError("Server does not support resource subscriptions")
}
let request = UnsubscribeRequest(uri: uri)
try await sendRequest(request)
}
// MARK: - Tool Methods
public func listTools() async throws -> ListToolsResponse {
guard serverCapabilities?.tools == true else {
throw MCPError.protocolError("Server does not support tools")
}
let request = ListToolsRequest()
return try await sendRequest(request)
}
public func callTool(name: String, arguments: [String: Any]? = nil) async throws -> CallToolResponse {
guard serverCapabilities?.tools == true else {
throw MCPError.protocolError("Server does not support tools")
}
let request = CallToolRequest(name: name, arguments: arguments)
return try await sendRequest(request)
}
private func sendRequest<T: MCPRequest, U: MCPResponse>(_ request: T) async throws -> U {
let data = try JSONEncoder().encode(request)
let responseData = try await transport.sendRequest(data)
return try JSONDecoder().decode(U.self, from: responseData)
}
private func sendNotification<T: MCPNotification>(_ notification: T) async throws {
let data = try JSONEncoder().encode(notification)
try await transport.sendNotification(data)
}
// MARK: - Public Properties
/// Get the server's capabilities after initialization
public var serverCapabilitiesAfterInit: ServerCapabilities? {
serverCapabilities
}
/// Get the server's version information after initialization
public var serverVersionAfterInit: Implementation? {
serverVersion
}
/// Get the server's instructions after initialization
public var serverInstructions: String? {
instructions
}
}
/// MCP Transport protocol
public protocol MCPTransport {
func connect() async throws
func disconnect() async
func sendRequest(_ data: Data) async throws -> Data
func sendNotification(_ data: Data) async throws
}
/// Basic notification types
public struct InitializedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/initialized"
}
public struct PingRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "ping"
public init(id: String = UUID().uuidString) {
self.id = id
}
}

View file

@ -1,166 +0,0 @@
import Foundation
/// Resource metadata
public struct ResourceMetadata: Codable {
public var name: String
public var uri: String
public var metadata: [String: String]?
public init(name: String, uri: String, metadata: [String: String]? = nil) {
self.name = name
self.uri = uri
self.metadata = metadata
}
}
/// Resource content types
public enum ResourceContent: Codable {
case text(String)
case binary(Data)
case embedded([EmbeddedResource])
private enum CodingKeys: String, CodingKey {
case type
case content
}
public 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 content = try container.decode(String.self, forKey: .content)
self = .text(content)
case "binary":
let content = try container.decode(Data.self, forKey: .content)
self = .binary(content)
case "embedded":
let content = try container.decode([EmbeddedResource].self, forKey: .content)
self = .embedded(content)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown resource type")
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .text(let content):
try container.encode("text", forKey: .type)
try container.encode(content, forKey: .content)
case .binary(let content):
try container.encode("binary", forKey: .type)
try container.encode(content, forKey: .content)
case .embedded(let content):
try container.encode("embedded", forKey: .type)
try container.encode(content, forKey: .content)
}
}
}
/// Embedded resource type
public struct EmbeddedResource: Codable {
public var type: String
public var data: String
public var metadata: [String: String]?
public init(type: String, data: String, metadata: [String: String]? = nil) {
self.type = type
self.data = data
self.metadata = metadata
}
}
/// Resource request types
public struct ListResourcesRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/list"
public init(id: String = UUID().uuidString) {
self.id = id
}
}
public struct ReadResourceRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/read"
public let params: ReadResourceParams
public struct ReadResourceParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = ReadResourceParams(uri: uri)
}
}
public struct SubscribeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/subscribe"
public let params: SubscribeParams
public struct SubscribeParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = SubscribeParams(uri: uri)
}
}
public struct UnsubscribeRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "resources/unsubscribe"
public let params: UnsubscribeParams
public struct UnsubscribeParams: Codable {
public let uri: String
}
public init(id: String = UUID().uuidString, uri: String) {
self.id = id
self.params = UnsubscribeParams(uri: uri)
}
}
/// Resource response types
public struct ListResourcesResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ListResourcesResult
public struct ListResourcesResult: Codable {
public let resources: [ResourceMetadata]
}
}
public struct ReadResourceResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ReadResourceResult
public struct ReadResourceResult: Codable {
public let content: ResourceContent
}
}
/// Resource notification types
public struct ResourceUpdatedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/resource/updated"
public let params: ResourceUpdatedParams
public struct ResourceUpdatedParams: Codable {
public let uri: String
public let content: ResourceContent
}
}

View file

@ -0,0 +1,206 @@
import Foundation
/// The Model Context Protocol (MCP) provides a standardized way
/// for servers to expose prompt templates to clients.
/// Prompts allow servers to provide structured messages and instructions
/// for interacting with language models.
/// Clients can discover available prompts, retrieve their contents,
/// and provide arguments to customize them.
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/
public struct Prompt: Hashable, Codable, Sendable {
/// The prompt name
public let name: String
/// The prompt description
public let description: String?
/// The prompt arguments
public let arguments: [Argument]?
public init(name: String, description: String? = nil, arguments: [Argument]? = nil) {
self.name = name
self.description = description
self.arguments = arguments
}
/// An argument for a prompt
public struct Argument: Hashable, Codable, Sendable {
/// The argument name
public let name: String
/// The argument description
public let description: String?
/// Whether the argument is required
public let required: Bool?
public init(name: String, description: String? = nil, required: Bool? = nil) {
self.name = name
self.description = description
self.required = required
}
}
/// A message in a prompt
public struct Message: Hashable, Codable, Sendable {
/// The message role
public enum Role: String, Hashable, Codable, Sendable {
/// A user message
case user
/// An assistant message
case assistant
}
/// The message role
public let role: Role
/// The message content
public let content: Content
public init(role: Role, content: Content) {
self.role = role
self.content = content
}
/// Content types for messages
public enum Content: Hashable, Codable, Sendable {
/// Text content
case text(text: String)
/// Image content
case image(data: String, mimeType: String)
/// Embedded resource content
case resource(uri: String, mimeType: String, text: String?, blob: String?)
private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType, uri, blob
}
public 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 .image(let data, let mimeType):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
case .resource(let uri, let mimeType, let text, let blob):
try container.encode("resource", forKey: .type)
try container.encode(uri, forKey: .uri)
try container.encode(mimeType, forKey: .mimeType)
try container.encodeIfPresent(text, forKey: .text)
try container.encodeIfPresent(blob, forKey: .blob)
}
}
public 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 "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .image(data: data, mimeType: mimeType)
case "resource":
let uri = try container.decode(String.self, forKey: .uri)
let mimeType = try container.decode(String.self, forKey: .mimeType)
let text = try container.decodeIfPresent(String.self, forKey: .text)
let blob = try container.decodeIfPresent(String.self, forKey: .blob)
self = .resource(uri: uri, mimeType: mimeType, text: text, blob: blob)
default:
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Unknown content type")
}
}
}
}
/// Reference type for prompts
public struct Reference: Hashable, Codable, Sendable {
/// The prompt reference name
public let name: String
public init(name: String) {
self.name = name
}
private enum CodingKeys: String, CodingKey {
case type, name
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("ref/prompt", forKey: .type)
try container.encode(name, forKey: .name)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
_ = try container.decode(String.self, forKey: .type)
name = try container.decode(String.self, forKey: .name)
}
}
}
// MARK: -
/// To retrieve available prompts, clients send a `prompts/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#listing-prompts
public enum ListPrompts: Method {
public static let name: String = "prompts/list"
public struct Parameters: Hashable, Codable, Sendable {
public let cursor: String?
public init(cursor: String? = nil) {
self.cursor = cursor
}
}
public struct Result: Hashable, Codable, Sendable {
public let prompts: [Prompt]
public let nextCursor: String?
public init(prompts: [Prompt], nextCursor: String? = nil) {
self.prompts = prompts
self.nextCursor = nextCursor
}
}
}
/// To retrieve a specific prompt, clients send a `prompts/get` request.
/// Arguments may be auto-completed through the completion API.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#getting-a-prompt
public enum GetPrompt: Method {
public static let name: String = "prompts/get"
public struct Parameters: Hashable, Codable, Sendable {
public let name: String
public let arguments: [String: Value]?
public init(name: String, arguments: [String: Value]? = nil) {
self.name = name
self.arguments = arguments
}
}
public struct Result: Hashable, Codable, Sendable {
public let description: String?
public let messages: [Prompt.Message]
public init(description: String?, messages: [Prompt.Message]) {
self.description = description
self.messages = messages
}
}
}
/// When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#list-changed-notification
public struct PromptListChangedNotification: Notification {
public static let name: String = "notifications/prompts/list_changed"
}

View file

@ -0,0 +1,195 @@
import Foundation
/// The Model Context Protocol (MCP) provides a standardized way
/// for servers to expose resources to clients.
/// Resources allow servers to share data that provides context to language models,
/// such as files, database schemas, or application-specific information.
/// Each resource is uniquely identified by a URI.
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/
public struct Resource: Hashable, Codable, Sendable {
/// The resource name
public var name: String
/// The resource URI
public var uri: String
/// The resource description
public var description: String?
/// The resource MIME type
public var mimeType: String?
/// The resource metadata
public var metadata: [String: String]?
public init(
name: String,
uri: String,
description: String? = nil,
mimeType: String? = nil,
metadata: [String: String]? = nil
) {
self.name = name
self.uri = uri
self.description = description
self.mimeType = mimeType
self.metadata = metadata
}
/// Content of a resource.
public struct Content: Hashable, Codable, Sendable {
/// The resource URI
public let uri: String
/// The resource MIME type
public let mimeType: String?
/// The resource text content
public let text: String?
/// The resource binary content
public let blob: String?
public static func text(_ content: String, uri: String, mimeType: String? = nil) -> Self {
.init(uri: uri, mimeType: mimeType, text: content)
}
public static func binary(_ data: Data, uri: String, mimeType: String? = nil) -> Self {
.init(uri: uri, mimeType: mimeType, blob: data.base64EncodedString())
}
private init(uri: String, mimeType: String? = nil, text: String? = nil) {
self.uri = uri
self.mimeType = mimeType
self.text = text
self.blob = nil
}
private init(uri: String, mimeType: String? = nil, blob: String) {
self.uri = uri
self.mimeType = mimeType
self.text = nil
self.blob = blob
}
}
/// A resource template.
public struct Template: Hashable, Codable, Sendable {
/// The URI template pattern
public var uriTemplate: String
/// The template name
public var name: String
/// The template description
public var description: String?
/// The resource MIME type
public var mimeType: String?
public init(
uriTemplate: String,
name: String,
description: String? = nil,
mimeType: String? = nil
) {
self.uriTemplate = uriTemplate
self.name = name
self.description = description
self.mimeType = mimeType
}
}
}
// MARK: -
/// To discover available resources, clients send a `resources/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#listing-resources
public enum ListResources: Method {
public static let name: String = "resources/list"
public struct Parameters: Hashable, Codable, Sendable {
public let cursor: String?
public init(cursor: String? = nil) {
self.cursor = cursor
}
}
public struct Result: Hashable, Codable, Sendable {
public let resources: [Resource]
public let nextCursor: String?
public init(resources: [Resource], nextCursor: String? = nil) {
self.resources = resources
self.nextCursor = nextCursor
}
}
}
/// To retrieve resource contents, clients send a `resources/read` request:
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#reading-resources
public enum ReadResource: Method {
public static let name: String = "resources/read"
public struct Parameters: Hashable, Codable, Sendable {
public let uri: String
public init(uri: String) {
self.uri = uri
}
}
public struct Result: Hashable, Codable, Sendable {
public let contents: [Resource.Content]
public init(contents: [Resource.Content]) {
self.contents = contents
}
}
}
/// To discover available resource templates, clients send a `resources/templates/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#resource-templates
public enum ListResourceTemplates: Method {
public static let name: String = "resources/templates/list"
public typealias Parameters = Empty
public struct Result: Hashable, Codable, Sendable {
public let templates: [Resource.Template]
public init(templates: [Resource.Template]) {
self.templates = templates
}
private enum CodingKeys: String, CodingKey {
case templates = "resourceTemplates"
}
}
}
/// When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#list-changed-notification
public struct ResourceListChangedNotification: Notification {
public static let name: String = "notifications/resources/list_changed"
public typealias Parameters = Empty
}
/// Clients can subscribe to specific resources and receive notifications when they change.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#subscriptions
public enum ResourceSubscribe: Method {
public static let name: String = "resources/subscribe"
public struct Parameters: Hashable, Codable, Sendable {
public let uri: String
}
public typealias Result = Empty
}
/// When a resource changes, servers that declared the updated capability SHOULD send a notification to subscribed clients.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#subscriptions
public struct ResourceUpdatedNotification: Notification {
public static let name: String = "notifications/resources/updated"
public struct Parameters: Hashable, Codable, Sendable {
public let uri: String
public init(uri: String) {
self.uri = uri
}
}
}

View file

@ -0,0 +1,393 @@
import Logging
import SystemPackage
import struct Foundation.Data
import struct Foundation.Date
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// Model Context Protocol server
public actor Server {
/// Implementation information
public struct Info: Hashable, Codable, Sendable {
/// The server name
public let name: String
/// The server version
public let version: String
public init(name: String, version: String) {
self.name = name
self.version = version
}
}
/// Server capabilities
public struct Capabilities: Hashable, Codable, Sendable {
/// Resources capabilities
public struct Resources: Hashable, Codable, Sendable {
/// Whether the list of resources has changed
public var list: Bool?
/// Whether the resource can be read
public var read: Bool?
/// Whether the resource can be subscribed to
public var subscribe: Bool?
/// Whether the list of resources has changed
public var listChanged: Bool?
public init(
list: Bool? = nil,
read: Bool? = nil,
subscribe: Bool? = nil,
listChanged: Bool? = nil
) {
self.list = list
self.read = read
self.subscribe = subscribe
self.listChanged = listChanged
}
}
/// Tools capabilities
public struct Tools: Hashable, Codable, Sendable {
/// Whether the server notifies clients when tools change
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Prompts capabilities
public struct Prompts: Hashable, Codable, Sendable {
/// Whether the server notifies clients when prompts change
public var listChanged: Bool?
public init(listChanged: Bool? = nil) {
self.listChanged = listChanged
}
}
/// Logging capabilities
public struct Logging: Hashable, Codable, Sendable {
public init() {}
}
/// Logging capabilities
public var logging: Logging?
/// Prompts capabilities
public var prompts: Prompts?
/// Resources capabilities
public var resources: Resources?
/// Tools capabilities
public var tools: Tools?
public init(
logging: Logging? = nil,
prompts: Prompts? = nil,
resources: Resources? = nil,
tools: Tools? = nil
) {
self.logging = logging
self.prompts = prompts
self.resources = resources
self.tools = tools
}
}
/// Server information
private let serverInfo: Server.Info
/// The server connection
private var connection: (any Transport)?
/// The server logger
private var logger: Logger? {
get async {
await connection?.logger
}
}
/// The server name
public nonisolated var name: String { serverInfo.name }
/// The server version
public nonisolated var version: String { serverInfo.version }
/// The server capabilities
public var capabilities = Capabilities()
/// Request handlers
private var methodHandlers: [String: RequestHandlerBox] = [:]
/// Notification handlers
private var notificationHandlers: [String: [NotificationHandlerBox]] = [:]
/// Whether the server is initialized
private var isInitialized = false
/// The client information
private var clientInfo: Client.Info?
/// The client capabilities
private var clientCapabilities: Client.Capabilities?
/// The protocol version
private var protocolVersion: String?
/// The list of subscriptions
private var subscriptions: [String: Set<ID>] = [:]
/// The task for the message handling loop
private var task: Task<Void, Never>?
public init(
name: String,
version: String,
capabilities: Server.Capabilities = .init()
) {
self.serverInfo = Server.Info(name: name, version: version)
self.capabilities = capabilities
}
/// Start the server
public func start(transport: any Transport) async throws {
self.connection = transport
registerDefaultHandlers()
try await transport.connect()
await logger?.info(
"Server started", metadata: ["name": "\(name)", "version": "\(version)"])
// Start message handling loop
task = Task {
do {
let stream = await transport.receive()
for try await string in stream {
if Task.isCancelled { break } // Check cancellation inside loop
var requestID: ID?
do {
guard let data = string.data(using: .utf8) else {
throw Error.parseError("Invalid UTF-8 data")
}
// Attempt to decode string data as AnyRequest or AnyMessage
let decoder = JSONDecoder()
if let request = try? decoder.decode(AnyRequest.self, from: data) {
try await handleRequest(request)
} else if let message = try? decoder.decode(AnyMessage.self, from: data) {
try await handleMessage(message)
} else {
// Try to extract request ID from raw JSON if possible
if let json = try? JSONDecoder().decode(
[String: Value].self, from: data),
let idValue = json["id"]
{
if let strValue = idValue.stringValue {
requestID = .string(strValue)
} else if let intValue = idValue.intValue {
requestID = .number(intValue)
}
}
throw Error.parseError("Invalid message format")
}
} catch let error as Errno where error == .resourceTemporarilyUnavailable {
// Resource temporarily unavailable, retry after a short delay
try? await Task.sleep(nanoseconds: 10_000_000) // 10ms
continue
} catch {
await logger?.error(
"Error processing message", metadata: ["error": "\(error)"])
// Only send error responses for requests
if let requestID = requestID {
let mcpError =
error as? Error ?? Error.internalError(error.localizedDescription)
let response = AnyMethod.response(id: requestID, error: mcpError)
try? await send(response)
}
}
}
} catch {
await logger?.error(
"Fatal error in message handling loop", metadata: ["error": "\(error)"])
}
}
}
/// Stop the server
public func stop() async {
task?.cancel()
task = nil
if let connection = connection {
await connection.disconnect()
}
connection = nil
}
// MARK: - Registration
/// Register a method handler
@discardableResult
public func withMethodHandler<M: Method>(
_ type: M.Type,
handler: @escaping @Sendable (M.Parameters) async throws -> M.Result
) -> Self {
methodHandlers[M.name] = TypedRequestHandler { (request: Request<M>) -> Response<M> in
let result = try await handler(request.params)
return Response(id: request.id, result: result)
}
return self
}
/// Register a notification handler
@discardableResult
public func onNotification<N: Notification>(
_ type: N.Type,
handler: @escaping @Sendable (Message<N>) async throws -> Void
) -> Self {
let handlers = notificationHandlers[N.name, default: []]
notificationHandlers[N.name] = handlers + [TypedNotificationHandler(handler)]
return self
}
// MARK: - Sending
/// Send a response to a client
public func send<M: Method>(_ response: Response<M>) async throws {
guard let connection = connection else {
throw Error.internalError("Server connection not initialized")
}
let responseData = try JSONEncoder().encode(response)
if let responseStr = String(data: responseData, encoding: .utf8) {
try await connection.send(responseStr)
}
}
/// Send a notification to connected clients
public func notify<N: Notification>(_ notification: Message<N>) async throws {
guard let connection = connection else {
throw Error.internalError("Server connection not initialized")
}
let notificationData = try JSONEncoder().encode(notification)
if let notificationStr = String(data: notificationData, encoding: .utf8) {
try await connection.send(notificationStr)
}
}
// MARK: -
private func handleRequest(_ request: Request<AnyMethod>) async throws {
await logger?.debug(
"Processing request",
metadata: [
"method": "\(request.method)",
"id": "\(request.id)",
])
// The client SHOULD NOT send requests other than pings
// before the server has responded to the initialize request.
switch request.method {
case Initialize.name, Ping.name:
break
default:
try checkInitialized()
}
// Find handler for method name
guard let handler = methodHandlers[request.method] else {
let error = Error.methodNotFound("Unknown method: \(request.method)")
let response = AnyMethod.response(id: request.id, error: error)
try await send(response)
throw error
}
do {
// Handle request and get response
let response = try await handler(request)
try await send(response)
} catch {
let mcpError = error as? Error ?? Error.internalError(error.localizedDescription)
let response = AnyMethod.response(id: request.id, error: mcpError)
try await send(response)
throw error
}
}
private func handleMessage(_ message: Message<AnyNotification>) async throws {
await logger?.debug(
"Processing notification",
metadata: ["method": "\(message.method)"])
// Check initialization state unless this is an initialized notification
if message.method != InitializedNotification.name {
try checkInitialized()
}
// Find notification handlers for this method
guard let handlers = notificationHandlers[message.method] else { return }
// Convert notification parameters to concrete type and call handlers
for handler in handlers {
do {
try await handler(message)
} catch {
await logger?.error(
"Error handling notification",
metadata: [
"method": "\(message.method)",
"error": "\(error)",
])
}
}
}
private func checkInitialized() throws {
guard isInitialized else {
throw Error.invalidRequest("Server is not initialized")
}
}
private func registerDefaultHandlers() {
// Initialize
withMethodHandler(Initialize.self) { [weak self] params in
guard let self = self else {
throw Error.internalError("Server was deallocated")
}
guard await !self.isInitialized else {
throw Error.invalidRequest("Server is already initialized")
}
// Validate protocol version
guard Version.latest == params.protocolVersion else {
throw Error.invalidRequest(
"Unsupported protocol version: \(params.protocolVersion)")
}
await self.setInitialState(
clientInfo: params.clientInfo,
clientCapabilities: params.capabilities,
protocolVersion: params.protocolVersion
)
let result = Initialize.Result(
protocolVersion: Version.latest,
capabilities: await self.capabilities,
serverInfo: self.serverInfo,
instructions: nil
)
// Send initialized notification after a short delay
Task {
try? await Task.sleep(nanoseconds: 100_000_000) // 100ms
try? await self.notify(InitializedNotification.message())
}
return result
}
// Ping
withMethodHandler(Ping.self) { _ in return Empty() }
}
private func setInitialState(
clientInfo: Client.Info,
clientCapabilities: Client.Capabilities,
protocolVersion: String
) async {
self.clientInfo = clientInfo
self.clientCapabilities = clientCapabilities
self.protocolVersion = protocolVersion
self.isInitialized = true
}
}

View file

@ -0,0 +1,170 @@
import Foundation
/// The Model Context Protocol (MCP) allows servers to expose tools
/// that can be invoked by language models.
/// Tools enable models to interact with external systems, such as
/// querying databases, calling APIs, or performing computations.
/// Each tool is uniquely identified by a name and includes metadata
/// describing its schema.
///
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
public struct Tool: Hashable, Codable, Sendable {
/// The tool name
public let name: String
/// The tool description
public let description: String
/// The tool input schema
public let inputSchema: Value?
public init(name: String, description: String, inputSchema: Value? = nil) {
self.name = name
self.description = description
self.inputSchema = inputSchema
}
/// Content types that can be returned by a tool
public enum Content: Hashable, Codable, Sendable {
/// Text content
case text(String)
/// Image content
case image(data: String, mimeType: String, metadata: [String: String]?)
/// Embedded resource content
case resource(uri: String, mimeType: String, text: String?)
private enum CodingKeys: String, CodingKey {
case type
case text
case image
case resource
case uri
case mimeType
case data
case metadata
}
public 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)
case "image":
let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType)
let metadata = try container.decodeIfPresent(
[String: String].self, forKey: .metadata)
self = .image(data: data, mimeType: mimeType, metadata: metadata)
case "resource":
let uri = try container.decode(String.self, forKey: .uri)
let mimeType = try container.decode(String.self, forKey: .mimeType)
let text = try container.decodeIfPresent(String.self, forKey: .text)
self = .resource(uri: uri, mimeType: mimeType, text: text)
default:
throw DecodingError.dataCorruptedError(
forKey: .type, in: container, debugDescription: "Unknown tool content type")
}
}
public 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 .image(let data, let mimeType, let metadata):
try container.encode("image", forKey: .type)
try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType)
try container.encodeIfPresent(metadata, forKey: .metadata)
case .resource(let uri, let mimeType, let text):
try container.encode("resource", forKey: .type)
try container.encode(uri, forKey: .uri)
try container.encode(mimeType, forKey: .mimeType)
try container.encodeIfPresent(text, forKey: .text)
}
}
}
private enum CodingKeys: String, CodingKey {
case name
case description
case inputSchema
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
description = try container.decode(String.self, forKey: .description)
inputSchema = try container.decodeIfPresent(Value.self, forKey: .inputSchema)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(description, forKey: .description)
if let schema = inputSchema {
try container.encode(schema, forKey: .inputSchema)
}
}
}
// MARK: -
/// To discover available tools, clients send a `tools/list` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#listing-tools
public enum ListTools: Method {
public static let name = "tools/list"
public struct Parameters: Hashable, Codable, Sendable {
public let cursor: String?
public init(cursor: String? = nil) {
self.cursor = cursor
}
}
public struct Result: Hashable, Codable, Sendable {
public let tools: [Tool]
public let nextCursor: String?
public init(tools: [Tool], nextCursor: String? = nil) {
self.tools = tools
self.nextCursor = nextCursor
}
}
}
/// To call a tool, clients send a `tools/call` request.
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#calling-tools
public enum CallTool: Method {
public static let name = "tools/call"
public struct Parameters: Hashable, Codable, Sendable {
public let name: String
public let arguments: [String: Value]?
public init(name: String, arguments: [String: Value]? = nil) {
self.name = name
self.arguments = arguments
}
}
public struct Result: Hashable, Codable, Sendable {
public let content: [Tool.Content]
public let isError: Bool
public init(content: [Tool.Content], isError: Bool) {
self.content = content
self.isError = isError
}
}
}
/// When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification:
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#list-changed-notification
public struct ToolListChangedNotification: Notification {
public static let name: String = "notifications/tools/list_changed"
}

View file

@ -1,105 +0,0 @@
import Foundation
/// A transport implementation that communicates over standard input/output
public actor StdioTransport: MCPTransport {
private var isConnected = false
private let processQueue = DispatchQueue(label: "com.mcp.stdio", qos: .userInitiated)
private let process: Process
private let inputPipe = Pipe()
private let outputPipe = Pipe()
private let errorPipe = Pipe()
public init(command: String, arguments: [String] = [], environment: [String: String]? = nil) {
self.process = Process()
self.process.executableURL = URL(fileURLWithPath: command)
self.process.arguments = arguments
if let env = environment {
self.process.environment = env
}
self.process.standardInput = inputPipe
self.process.standardOutput = outputPipe
self.process.standardError = errorPipe
}
public func connect() async throws {
guard !isConnected else { return }
try process.run()
isConnected = true
// Start reading output asynchronously
Task {
await readOutput()
}
}
public func disconnect() async {
guard isConnected else { return }
process.terminate()
isConnected = false
}
public func sendRequest(_ data: Data) async throws -> Data {
guard isConnected else {
throw MCPError.connectionClosed
}
// Write request
try await withCheckedThrowingContinuation { continuation in
processQueue.async {
do {
let message = data + "\n".data(using: .utf8)!
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
// Read response
if let response = try self.outputPipe.fileHandleForReading.readToEnd() {
continuation.resume(returning: response)
} else {
continuation.resume(throwing: MCPError.invalidResponse)
}
} catch {
continuation.resume(throwing: MCPError.transportError(error))
}
}
}
}
public func sendNotification(_ data: Data) async throws {
guard isConnected else {
throw MCPError.connectionClosed
}
try await withCheckedThrowingContinuation { continuation in
processQueue.async {
do {
let message = data + "\n".data(using: .utf8)!
try self.inputPipe.fileHandleForWriting.write(contentsOf: message)
continuation.resume()
} catch {
continuation.resume(throwing: MCPError.transportError(error))
}
}
}
}
private func readOutput() async {
let handle = outputPipe.fileHandleForReading
while isConnected {
do {
let data = try handle.read(upToCount: 4096)
if let data = data, !data.isEmpty {
// Process received data
// In a real implementation, you would parse the JSON-RPC messages
// and handle them appropriately
print("Received data: \(String(data: data, encoding: .utf8) ?? "")")
}
} catch {
print("Error reading output: \(error)")
break
}
}
}
}

View file

@ -1,175 +0,0 @@
import Foundation
/// Tool metadata
public struct Tool: Codable {
public let name: String
public let description: String?
public let inputSchema: [String: Any]?
enum CodingKeys: String, CodingKey {
case name
case description
case inputSchema
}
public init(name: String, description: String? = nil, inputSchema: [String: Any]? = nil) {
self.name = name
self.description = description
self.inputSchema = inputSchema
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
description = try container.decodeIfPresent(String.self, forKey: .description)
inputSchema = try container.decodeIfPresent([String: Any].self, forKey: .inputSchema)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encodeIfPresent(description, forKey: .description)
if let schema = inputSchema {
try container.encode(schema, forKey: .inputSchema)
}
}
}
/// Tool request types
public struct ListToolsRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "tools/list"
public init(id: String = UUID().uuidString) {
self.id = id
}
}
public struct CallToolRequest: MCPRequest {
public let jsonrpc: String = "2.0"
public let id: String
public let method: String = "tools/call"
public let params: CallToolParams
public struct CallToolParams: Codable {
public let name: String
public let arguments: [String: Any]?
enum CodingKeys: String, CodingKey {
case name
case arguments
}
public init(name: String, arguments: [String: Any]? = nil) {
self.name = name
self.arguments = arguments
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
arguments = try container.decodeIfPresent([String: Any].self, forKey: .arguments)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
if let args = arguments {
try container.encode(args, forKey: .arguments)
}
}
}
public init(id: String = UUID().uuidString, name: String, arguments: [String: Any]? = nil) {
self.id = id
self.params = CallToolParams(name: name, arguments: arguments)
}
}
/// Tool response types
public struct ListToolsResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: ListToolsResult
public struct ListToolsResult: Codable {
public let tools: [Tool]
}
}
public struct CallToolResponse: MCPResponse {
public let jsonrpc: String = "2.0"
public let id: String
public let result: CallToolResult
public struct CallToolResult: Codable {
public let content: [ToolContent]
public let isError: Bool?
public init(content: [ToolContent], isError: Bool? = nil) {
self.content = content
self.isError = isError
}
}
}
/// Tool content types
public enum ToolContent: Codable {
case text(String)
case image(ImageContent)
private enum CodingKeys: String, CodingKey {
case type
case text
case image
}
public 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)
case "image":
let image = try container.decode(ImageContent.self, forKey: .image)
self = .image(image)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown tool content type")
}
}
public 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 .image(let image):
try container.encode("image", forKey: .type)
try container.encode(image, forKey: .image)
}
}
}
/// Image content type
public struct ImageContent: Codable {
public let data: String
public let mimeType: String
public let metadata: [String: String]?
public init(data: String, mimeType: String, metadata: [String: String]? = nil) {
self.data = data
self.mimeType = mimeType
self.metadata = metadata
}
}
/// Tool notification types
public struct ToolListChangedNotification: MCPNotification {
public let jsonrpc: String = "2.0"
public let method: String = "notifications/tools/list_changed"
}