V0.11.x (#199)
* Support 2025-06-18 Spec (#167) * Support OpenAI _meta for Tool * Update init * Add title and outputSchema * Concurreny issue * General fields * Add Generate Fields to Server elements * 4 tab spacing * Update Version * Elicitation * Format * More tests and inits * Elicitation in README * Fix orphaned test * Add title to more structs * Public init * Fix meta validation and make tests more generic * Format and make Result properties public again * Revert more visibility changes * feat: 2025-11-25 phase 1, includes Version, Icon, Progress (#181) * chore: fixed unit tests * feat: added icon and progess base structs * feat: added progress notification and meta to callTool * feat: added tests * feat: simplified RequestMeta encoding/decoding * tests: added unit test to track progress * chore: rebased v0.11.x * feat: implemented cancellation * feat: added overloaded callTool method * fix: encoding Response _meta fix * chore: added old version to the versions list * feat: 2025-11-25 phase 2; added Logging and Completions (#196) * feat: added Logging and Completions * feat: updated EmbeddedResource for the latest spec * chore: changed encoding of meta in Prompts to encodeIfPresent * feat: compliance tests + http server transport (#197) * feat: http server transport * feat: added http server transport * feat: removed unused files * feat: added conformance tests * feat: improved conformace testing * chore: removed redundant files * feat/2025-11-25: sampling, elicitation, roots (#198) * feat: Roots implementation * feat: added unit testing * feat: improved unit tests and fixed NetworkTransport crash * feat: implemented sampling and elicitation * feat: improved sampling tests * feat: improved elicitation tests * feat: improved confirmance tests * feat: improved Prompts unit tests * feat: removed test code * feat: updated conformance tests * chore: removed redundant commas * chore: removed 6.0.3 swift version from ci * chore: improved unit tests a little bit * chore: update README.md * chore: add server progress tracking example to README.md * chore: updated README.md * chore: updated transports table formatting * chore: updated MockNetworkConnection --------- Co-authored-by: Austin Evans <austinjevans@me.com> Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
This commit is contained in:
parent
6fa1a97b41
commit
6112a3995a
4 changed files with 11854 additions and 828 deletions
|
|
@ -34,11 +34,14 @@ public actor Client {
|
|||
public struct Info: Hashable, Codable, Sendable {
|
||||
/// The client name
|
||||
public var name: String
|
||||
/// A human-readable title for display purposes
|
||||
public var title: String?
|
||||
/// The client version
|
||||
public var version: String
|
||||
|
||||
public init(name: String, version: String) {
|
||||
public init(name: String, version: String, title: String? = nil) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.version = version
|
||||
}
|
||||
}
|
||||
|
|
@ -56,12 +59,55 @@ public actor Client {
|
|||
}
|
||||
|
||||
/// The sampling capabilities
|
||||
public struct Sampling: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
public struct Sampling: Hashable, Sendable {
|
||||
/// Tools sub-capability for sampling
|
||||
public struct Tools: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// Context sub-capability for sampling
|
||||
public struct Context: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// Whether tools are supported in sampling
|
||||
public var tools: Tools?
|
||||
/// Whether context is supported in sampling
|
||||
public var context: Context?
|
||||
|
||||
public init(tools: Tools? = nil, context: Context? = nil) {
|
||||
self.tools = tools
|
||||
self.context = context
|
||||
}
|
||||
}
|
||||
|
||||
/// The elicitation capabilities
|
||||
public struct Elicitation: Hashable, Sendable {
|
||||
/// Form-based elicitation sub-capability
|
||||
public struct Form: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// URL-based elicitation sub-capability
|
||||
public struct URL: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// Whether form-based elicitation is supported
|
||||
public var form: Form?
|
||||
/// Whether URL-based elicitation is supported
|
||||
public var url: URL?
|
||||
|
||||
public init(form: Form? = Form(), url: URL? = nil) {
|
||||
self.form = form
|
||||
self.url = url
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the client supports sampling
|
||||
public var sampling: Sampling?
|
||||
/// Whether the client supports elicitation
|
||||
public var elicitation: Elicitation?
|
||||
/// Experimental features supported by the client
|
||||
public var experimental: [String: String]?
|
||||
/// Whether the client supports roots
|
||||
|
|
@ -69,10 +115,12 @@ public actor Client {
|
|||
|
||||
public init(
|
||||
sampling: Sampling? = nil,
|
||||
elicitation: Elicitation? = nil,
|
||||
experimental: [String: String]? = nil,
|
||||
roots: Capabilities.Roots? = nil
|
||||
) {
|
||||
self.sampling = sampling
|
||||
self.elicitation = elicitation
|
||||
self.experimental = experimental
|
||||
self.roots = roots
|
||||
}
|
||||
|
|
@ -91,6 +139,8 @@ public actor Client {
|
|||
private let clientInfo: Client.Info
|
||||
/// The client name
|
||||
public nonisolated var name: String { clientInfo.name }
|
||||
/// A human-readable client title
|
||||
public nonisolated var title: String? { clientInfo.title }
|
||||
/// The client version
|
||||
public nonisolated var version: String { clientInfo.version }
|
||||
|
||||
|
|
@ -108,6 +158,8 @@ public actor Client {
|
|||
|
||||
/// A dictionary of type-erased notification handlers, keyed by method name
|
||||
private var notificationHandlers: [String: [NotificationHandlerBox]] = [:]
|
||||
/// Method handlers for server-to-client requests
|
||||
private var methodHandlers: [String: RequestHandlerBox] = [:]
|
||||
/// The task for the message handling loop
|
||||
private var task: Task<Void, Never>?
|
||||
|
||||
|
|
@ -120,8 +172,8 @@ public actor Client {
|
|||
}
|
||||
|
||||
/// A type-erased pending request
|
||||
private struct AnyPendingRequest {
|
||||
private let _resume: (Result<Any, Swift.Error>) -> Void
|
||||
private struct AnyPendingRequest: Sendable {
|
||||
private let _resume: @Sendable (Result<Any, Swift.Error>) -> Void
|
||||
|
||||
init<T: Sendable & Decodable>(_ request: PendingRequest<T>) {
|
||||
_resume = { result in
|
||||
|
|
@ -160,10 +212,12 @@ public actor Client {
|
|||
public init(
|
||||
name: String,
|
||||
version: String,
|
||||
title: String? = nil,
|
||||
capabilities: Capabilities = Capabilities(),
|
||||
configuration: Configuration = .default
|
||||
) {
|
||||
self.clientInfo = Client.Info(name: name, version: version)
|
||||
self.capabilities = Capabilities()
|
||||
self.clientInfo = Client.Info(name: name, version: version, title: title)
|
||||
self.capabilities = capabilities
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
|
|
@ -194,6 +248,8 @@ public actor Client {
|
|||
await handleBatchResponse(batchResponse)
|
||||
} else if let response = try? decoder.decode(AnyResponse.self, from: data) {
|
||||
await handleResponse(response)
|
||||
} else if let request = try? decoder.decode(AnyRequest.self, from: data) {
|
||||
await handleIncomingRequest(request)
|
||||
} else if let message = try? decoder.decode(AnyMessage.self, from: data) {
|
||||
await handleMessage(message)
|
||||
} else {
|
||||
|
|
@ -219,6 +275,27 @@ public actor Client {
|
|||
await self.logger?.debug("Client message handling loop task is terminating.")
|
||||
}
|
||||
|
||||
// Register cancellation notification handler
|
||||
await self.onNotification(CancelledNotification.self) { [weak self] message in
|
||||
guard let self = self else { return }
|
||||
|
||||
let requestId = message.params.requestId
|
||||
let reason = message.params.reason
|
||||
|
||||
await self.logger?.debug(
|
||||
"Received cancellation notification",
|
||||
metadata: [
|
||||
"requestId": "\(requestId)",
|
||||
"reason": reason.map { "\($0)" } ?? "none",
|
||||
]
|
||||
)
|
||||
|
||||
// Remove the pending request and resume with cancellation error
|
||||
if let pendingRequest = await self.removePendingRequest(id: requestId) {
|
||||
pendingRequest.resume(throwing: CancellationError())
|
||||
}
|
||||
}
|
||||
|
||||
// Automatically initialize after connecting
|
||||
return try await _initialize()
|
||||
}
|
||||
|
|
@ -277,6 +354,19 @@ public actor Client {
|
|||
return self
|
||||
}
|
||||
|
||||
/// Register a handler for server-to-client requests
|
||||
@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
|
||||
}
|
||||
|
||||
/// Send a notification to the server
|
||||
public func notify<N: Notification>(_ notification: Message<N>) async throws {
|
||||
guard let connection = connection else {
|
||||
|
|
@ -287,43 +377,108 @@ public actor Client {
|
|||
try await connection.send(notificationData)
|
||||
}
|
||||
|
||||
/// Send a response back to the server for a server-to-client request
|
||||
private func send<M: Method>(_ response: Response<M>) async throws {
|
||||
guard let connection = connection else {
|
||||
throw MCPError.internalError("Client connection not initialized")
|
||||
}
|
||||
let responseData = try encoder.encode(response)
|
||||
try await connection.send(responseData)
|
||||
}
|
||||
|
||||
// MARK: - Requests
|
||||
|
||||
/// Send a request and receive its response
|
||||
public func send<M: Method>(_ request: Request<M>) async throws -> M.Result {
|
||||
/// Send a request and return a RequestContext that wraps both the request ID and the Task.
|
||||
///
|
||||
/// This allows you to track and cancel the request by sending a CancelledNotification
|
||||
/// to the server using the requestID.
|
||||
///
|
||||
/// Example:
|
||||
/// ```swift
|
||||
/// let context = try await client.send(request)
|
||||
/// // Later, to cancel:
|
||||
/// try await client.cancelRequest(context.requestID, reason: "User cancelled")
|
||||
/// // Await the result:
|
||||
/// let result = try await context.value
|
||||
/// ```
|
||||
///
|
||||
/// - Parameter request: The request to send
|
||||
/// - Returns: A RequestContext containing the request ID and Task
|
||||
/// - Throws: MCPError if the client is not connected
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
|
||||
public func send<M: Method>(_ request: Request<M>) throws -> RequestContext<M.Result> {
|
||||
guard let connection = connection else {
|
||||
throw MCPError.internalError("Client connection not initialized")
|
||||
}
|
||||
|
||||
let requestData = try encoder.encode(request)
|
||||
|
||||
// Store the pending request first
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
Task {
|
||||
// Add the pending request before attempting to send
|
||||
self.addPendingRequest(
|
||||
id: request.id,
|
||||
continuation: continuation,
|
||||
type: M.Result.self
|
||||
)
|
||||
let requestTask = Task<M.Result, Error> {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
Task {
|
||||
// Add the pending request before attempting to send
|
||||
self.addPendingRequest(
|
||||
id: request.id,
|
||||
continuation: continuation,
|
||||
type: M.Result.self
|
||||
)
|
||||
|
||||
// Send the request data
|
||||
do {
|
||||
// Use the existing connection send
|
||||
try await connection.send(requestData)
|
||||
} catch {
|
||||
// If send fails, try to remove the pending request.
|
||||
// Resume with the send error only if we successfully removed the request,
|
||||
// indicating the response handler hasn't processed it yet.
|
||||
if self.removePendingRequest(id: request.id) != nil {
|
||||
continuation.resume(throwing: error)
|
||||
// Send the request data
|
||||
do {
|
||||
try await connection.send(requestData)
|
||||
} catch {
|
||||
// If send fails, try to remove the pending request.
|
||||
if self.removePendingRequest(id: request.id) != nil {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
// Otherwise, the request was already removed by the response handler
|
||||
// or by disconnect, so the continuation was already resumed.
|
||||
// Do nothing here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RequestContext(requestID: request.id, requestTask: requestTask)
|
||||
}
|
||||
|
||||
/// Cancel a request by sending a CancelledNotification to the server.
|
||||
///
|
||||
/// According to the MCP specification, cancellation is advisory:
|
||||
/// - The server SHOULD stop processing and free resources
|
||||
/// - The server MAY ignore the cancellation if the request is unknown, already completed,
|
||||
/// or cannot be cancelled
|
||||
/// - The client SHOULD ignore any response that arrives after cancellation
|
||||
///
|
||||
/// This method removes the pending request and resumes it with CancellationError,
|
||||
/// ensuring that any response arriving after cancellation is ignored.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - requestID: The ID of the request to cancel
|
||||
/// - reason: An optional human-readable reason for the cancellation
|
||||
/// - Throws: MCPError if the notification cannot be sent
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/cancellation
|
||||
public func cancelRequest(_ requestID: ID, reason: String? = nil) async throws {
|
||||
// Remove the pending request and resume with cancellation error
|
||||
// This ensures any response that arrives after cancellation is ignored
|
||||
if let pendingRequest = removePendingRequest(id: requestID) {
|
||||
pendingRequest.resume(throwing: CancellationError())
|
||||
}
|
||||
|
||||
// Send cancellation notification to server
|
||||
let notification = CancelledNotification.message(
|
||||
.init(requestId: requestID, reason: reason)
|
||||
)
|
||||
try await notify(notification)
|
||||
}
|
||||
|
||||
/// Send a request and receive its response immediately.
|
||||
///
|
||||
/// Internal convenience method for cases where cancellation tracking is not needed.
|
||||
///
|
||||
/// - Parameter request: The request to send
|
||||
/// - Returns: The result of the request
|
||||
/// - Throws: MCPError if the client is not connected
|
||||
func sendAndAwait<M: Method>(_ request: Request<M>) async throws -> M.Result {
|
||||
let context = try send(request)
|
||||
return try await context.value
|
||||
}
|
||||
|
||||
private func addPendingRequest<T: Sendable & Decodable>(
|
||||
|
|
@ -331,7 +486,9 @@ public actor Client {
|
|||
continuation: CheckedContinuation<T, Swift.Error>,
|
||||
type: T.Type // Keep type for AnyPendingRequest internal logic
|
||||
) {
|
||||
pendingRequests[id] = AnyPendingRequest(PendingRequest(continuation: continuation))
|
||||
pendingRequests[id] = AnyPendingRequest(
|
||||
PendingRequest(continuation: continuation)
|
||||
)
|
||||
}
|
||||
|
||||
private func removePendingRequest(id: ID) -> AnyPendingRequest? {
|
||||
|
|
@ -452,7 +609,7 @@ public actor Client {
|
|||
/// Use this object to add requests to the batch.
|
||||
/// - Throws: `MCPError.internalError` if the client is not connected.
|
||||
/// Can also rethrow errors from the `body` closure or from sending the batch request.
|
||||
public func withBatch(body: @escaping (Batch) async throws -> Void) async throws {
|
||||
public func withBatch(body: @escaping @Sendable (Batch) async throws -> Void) async throws {
|
||||
guard let connection = connection else {
|
||||
throw MCPError.internalError("Client connection not initialized")
|
||||
}
|
||||
|
|
@ -508,7 +665,7 @@ public actor Client {
|
|||
clientInfo: clientInfo
|
||||
))
|
||||
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
|
||||
self.serverCapabilities = result.capabilities
|
||||
self.serverVersion = result.protocolVersion
|
||||
|
|
@ -521,7 +678,7 @@ public actor Client {
|
|||
|
||||
public func ping() async throws {
|
||||
let request = Ping.request()
|
||||
_ = try await send(request)
|
||||
_ = try await sendAndAwait(request)
|
||||
}
|
||||
|
||||
// MARK: - Prompts
|
||||
|
|
@ -531,7 +688,7 @@ public actor Client {
|
|||
{
|
||||
try validateServerCapability(\.prompts, "Prompts")
|
||||
let request = GetPrompt.request(.init(name: name, arguments: arguments))
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
return (description: result.description, messages: result.messages)
|
||||
}
|
||||
|
||||
|
|
@ -545,7 +702,7 @@ public actor Client {
|
|||
} else {
|
||||
request = ListPrompts.request(.init())
|
||||
}
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
return (prompts: result.prompts, nextCursor: result.nextCursor)
|
||||
}
|
||||
|
||||
|
|
@ -554,7 +711,7 @@ public actor Client {
|
|||
public func readResource(uri: String) async throws -> [Resource.Content] {
|
||||
try validateServerCapability(\.resources, "Resources")
|
||||
let request = ReadResource.request(.init(uri: uri))
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
return result.contents
|
||||
}
|
||||
|
||||
|
|
@ -568,14 +725,14 @@ public actor Client {
|
|||
} else {
|
||||
request = ListResources.request(.init())
|
||||
}
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
return (resources: result.resources, nextCursor: result.nextCursor)
|
||||
}
|
||||
|
||||
public func subscribeToResource(uri: String) async throws {
|
||||
try validateServerCapability(\.resources?.subscribe, "Resource subscription")
|
||||
let request = ResourceSubscribe.request(.init(uri: uri))
|
||||
_ = try await send(request)
|
||||
_ = try await sendAndAwait(request)
|
||||
}
|
||||
|
||||
public func listResourceTemplates(cursor: String? = nil) async throws -> (
|
||||
|
|
@ -588,7 +745,7 @@ public actor Client {
|
|||
} else {
|
||||
request = ListResourceTemplates.request(.init())
|
||||
}
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
return (templates: result.templates, nextCursor: result.nextCursor)
|
||||
}
|
||||
|
||||
|
|
@ -604,19 +761,53 @@ public actor Client {
|
|||
} else {
|
||||
request = ListTools.request(.init())
|
||||
}
|
||||
let result = try await send(request)
|
||||
let result = try await sendAndAwait(request)
|
||||
return (tools: result.tools, nextCursor: result.nextCursor)
|
||||
}
|
||||
|
||||
public func callTool(name: String, arguments: [String: Value]? = nil) async throws -> (
|
||||
content: [Tool.Content], isError: Bool?
|
||||
) {
|
||||
/// Call a tool on the server.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the tool to call.
|
||||
/// - arguments: Arguments to use for the tool call.
|
||||
/// - meta: Optional request metadata including progress token. If `progressToken` is specified,
|
||||
/// the caller is requesting out-of-band progress notifications for this request.
|
||||
/// Use `onNotification(ProgressNotification.self)` to receive progress updates.
|
||||
/// - Returns: A tuple containing the tool's content response and an optional error flag.
|
||||
/// - Note: For advanced use cases requiring cancellation support, use `send()` directly to get a `RequestContext`.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/tools/#calling-tools
|
||||
public func callTool(
|
||||
name: String,
|
||||
arguments: [String: Value]? = nil,
|
||||
meta: Metadata? = nil
|
||||
) async throws -> (content: [Tool.Content], isError: Bool?) {
|
||||
try validateServerCapability(\.tools, "Tools")
|
||||
let request = CallTool.request(.init(name: name, arguments: arguments))
|
||||
let result = try await send(request)
|
||||
let request = CallTool.request(.init(name: name, arguments: arguments, meta: meta))
|
||||
let result = try await sendAndAwait(request)
|
||||
return (content: result.content, isError: result.isError)
|
||||
}
|
||||
|
||||
/// Call a tool on the server.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - name: The name of the tool to call.
|
||||
/// - arguments: Arguments to use for the tool call.
|
||||
/// - meta: Optional request metadata including progress token. If `progressToken` is specified,
|
||||
/// the caller is requesting out-of-band progress notifications for this request.
|
||||
/// Use `onNotification(ProgressNotification.self)` to receive progress updates.
|
||||
/// - Returns: A tuple containing the tool's content response and an optional error flag.
|
||||
/// - Note: For advanced use cases requiring cancellation support, use `send()` directly to get a `RequestContext`.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/tools/#calling-tools
|
||||
public func callTool(
|
||||
name: String,
|
||||
arguments: [String: Value]? = nil,
|
||||
meta: Metadata? = nil
|
||||
) throws -> RequestContext<CallTool.Result> {
|
||||
try validateServerCapability(\.tools, "Tools")
|
||||
let request = CallTool.request(.init(name: name, arguments: arguments, meta: meta))
|
||||
return try send(request)
|
||||
}
|
||||
|
||||
// MARK: - Sampling
|
||||
|
||||
/// Register a handler for sampling requests from servers
|
||||
|
|
@ -636,24 +827,145 @@ public actor Client {
|
|||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
|
||||
@discardableResult
|
||||
public func withSamplingHandler(
|
||||
_ handler: @escaping @Sendable (CreateSamplingMessage.Parameters) async throws ->
|
||||
_ handler:
|
||||
@escaping @Sendable (CreateSamplingMessage.Parameters) async throws ->
|
||||
CreateSamplingMessage.Result
|
||||
) -> Self {
|
||||
// Note: This would require extending the client architecture to handle incoming requests from servers.
|
||||
// The current MCP Swift SDK architecture assumes clients only send requests to servers,
|
||||
// but sampling requires bidirectional communication where servers can send requests to clients.
|
||||
//
|
||||
// A full implementation would need:
|
||||
// 1. Request handlers in the client (similar to how servers handle requests)
|
||||
// 2. Bidirectional transport support
|
||||
// 3. Request/response correlation for server-to-client requests
|
||||
//
|
||||
// For now, this serves as the correct API design for when bidirectional support is added.
|
||||
return withMethodHandler(CreateSamplingMessage.self, handler: handler)
|
||||
}
|
||||
|
||||
// This would register the handler similar to how servers register method handlers:
|
||||
// methodHandlers[CreateSamplingMessage.name] = TypedRequestHandler(handler)
|
||||
// MARK: - Elicitation
|
||||
|
||||
return self
|
||||
/// Register a handler for elicitation requests from servers
|
||||
///
|
||||
/// The elicitation flow lets servers collect structured input from users during
|
||||
/// ongoing interactions. Clients remain in control by mediating the prompt,
|
||||
/// collecting the response, and returning the chosen action to the server.
|
||||
///
|
||||
/// - Parameter handler: A closure that processes elicitation requests and returns user actions
|
||||
/// - Returns: Self for method chaining
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation
|
||||
@discardableResult
|
||||
public func withElicitationHandler(
|
||||
_ handler:
|
||||
@escaping @Sendable (CreateElicitation.Parameters) async throws ->
|
||||
CreateElicitation.Result
|
||||
) -> Self {
|
||||
return withMethodHandler(CreateElicitation.self, handler: handler)
|
||||
}
|
||||
|
||||
// MARK: - Roots
|
||||
|
||||
/// Register a handler for roots/list requests from servers
|
||||
///
|
||||
/// Roots define filesystem boundaries that servers can operate within.
|
||||
/// Unlike other MCP features, roots use bidirectional communication where
|
||||
/// servers send requests TO clients to discover available roots.
|
||||
///
|
||||
/// - Parameter handler: A closure that returns the list of available roots
|
||||
/// - Returns: Self for method chaining
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/client/roots
|
||||
@discardableResult
|
||||
public func withRootsHandler(
|
||||
_ handler: @escaping @Sendable () async throws -> [Root]
|
||||
) -> Self {
|
||||
return withMethodHandler(ListRoots.self) { _ in
|
||||
let roots = try await handler()
|
||||
return ListRoots.Result(roots: roots)
|
||||
}
|
||||
}
|
||||
|
||||
/// Notify the server that the list of roots has changed
|
||||
///
|
||||
/// Clients should send this notification when roots are added, removed,
|
||||
/// or modified to inform connected servers of the change.
|
||||
///
|
||||
/// - Throws: MCPError if the client is not connected
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/client/roots
|
||||
public func notifyRootsChanged() async throws {
|
||||
let notification = RootsListChangedNotification.message()
|
||||
try await notify(notification)
|
||||
}
|
||||
|
||||
// MARK: - Logging
|
||||
|
||||
/// Set the minimum logging level for server log messages.
|
||||
///
|
||||
/// Servers that declare the `logging` capability will send log messages via
|
||||
/// `notifications/message` notifications. Use this method to control which
|
||||
/// severity levels the server should send.
|
||||
///
|
||||
/// - Parameter level: The minimum log level to receive
|
||||
/// - Throws: MCPError if the client is not connected or if the server doesn't support logging
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging/
|
||||
public func setLoggingLevel(_ level: LogLevel) async throws {
|
||||
try validateServerCapability(\.logging, "Logging")
|
||||
let request = SetLoggingLevel.request(.init(level: level))
|
||||
_ = try await sendAndAwait(request)
|
||||
}
|
||||
|
||||
// MARK: - Completions
|
||||
|
||||
/// Request completion suggestions for a prompt argument.
|
||||
///
|
||||
/// Servers that declare the `completions` capability can provide autocompletion
|
||||
/// suggestions for prompt arguments as users type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - promptName: The name of the prompt
|
||||
/// - argumentName: The name of the argument being completed
|
||||
/// - argumentValue: The current (partial) value of the argument
|
||||
/// - context: Optional context with already-resolved arguments
|
||||
/// - Returns: A completion result containing suggested values
|
||||
/// - Throws: MCPError if the client is not connected or if the server doesn't support completions
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion/
|
||||
public func complete(
|
||||
promptName: String,
|
||||
argumentName: String,
|
||||
argumentValue: String,
|
||||
context: [String: Value]? = nil
|
||||
) async throws -> Complete.Result.Completion {
|
||||
try validateServerCapability(\.completions, "Completions")
|
||||
let request = Complete.request(
|
||||
.init(
|
||||
ref: .prompt(.init(name: promptName)),
|
||||
argument: .init(name: argumentName, value: argumentValue),
|
||||
context: context.map { .init(arguments: $0) }
|
||||
)
|
||||
)
|
||||
let result = try await sendAndAwait(request)
|
||||
return result.completion
|
||||
}
|
||||
|
||||
/// Request completion suggestions for a resource template argument.
|
||||
///
|
||||
/// Servers that declare the `completions` capability can provide autocompletion
|
||||
/// suggestions for resource template arguments as users type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - resourceURI: The URI of the resource template
|
||||
/// - argumentName: The name of the argument being completed
|
||||
/// - argumentValue: The current (partial) value of the argument
|
||||
/// - context: Optional context with already-resolved arguments
|
||||
/// - Returns: A completion result containing suggested values
|
||||
/// - Throws: MCPError if the client is not connected or if the server doesn't support completions
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion/
|
||||
public func complete(
|
||||
resourceURI: String,
|
||||
argumentName: String,
|
||||
argumentValue: String,
|
||||
context: [String: Value]? = nil
|
||||
) async throws -> Complete.Result.Completion {
|
||||
try validateServerCapability(\.completions, "Completions")
|
||||
let request = Complete.request(
|
||||
.init(
|
||||
ref: .resource(.init(uri: resourceURI)),
|
||||
argument: .init(name: argumentName, value: argumentValue),
|
||||
context: context.map { .init(arguments: $0) }
|
||||
)
|
||||
)
|
||||
let result = try await sendAndAwait(request)
|
||||
return result.completion
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
|
@ -706,6 +1018,43 @@ public actor Client {
|
|||
}
|
||||
}
|
||||
|
||||
private func handleIncomingRequest(_ request: Request<AnyMethod>) async {
|
||||
await logger?.trace(
|
||||
"Processing incoming request from server",
|
||||
metadata: ["method": "\(request.method)", "id": "\(request.id)"])
|
||||
|
||||
guard let handler = methodHandlers[request.method] else {
|
||||
await logger?.warning(
|
||||
"No handler registered for method",
|
||||
metadata: ["method": "\(request.method)"])
|
||||
let error = MCPError.methodNotFound("Unknown method: \(request.method)")
|
||||
let response = AnyMethod.response(id: request.id, error: error)
|
||||
do {
|
||||
try await send(response)
|
||||
} catch {
|
||||
await logger?.error(
|
||||
"Failed to send error response",
|
||||
metadata: ["error": "\(error)"])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await handler(request)
|
||||
try await send(response)
|
||||
} catch {
|
||||
let mcpError = error as? MCPError ?? MCPError.internalError(error.localizedDescription)
|
||||
let response = AnyMethod.response(id: request.id, error: mcpError)
|
||||
do {
|
||||
try await send(response)
|
||||
} catch {
|
||||
await logger?.error(
|
||||
"Failed to send error response",
|
||||
metadata: ["error": "\(error)"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// Validate the server capabilities.
|
||||
|
|
@ -751,3 +1100,57 @@ public actor Client {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
extension Client.Capabilities.Sampling: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case tools, context
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Handle both empty object {} and object with sub-capabilities
|
||||
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
|
||||
self.tools = try container.decodeIfPresent(Tools.self, forKey: .tools)
|
||||
self.context = try container.decodeIfPresent(Context.self, forKey: .context)
|
||||
} else {
|
||||
// Empty object - no capabilities
|
||||
self.tools = nil
|
||||
self.context = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(tools, forKey: .tools)
|
||||
try container.encodeIfPresent(context, forKey: .context)
|
||||
}
|
||||
}
|
||||
|
||||
extension Client.Capabilities.Elicitation: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case form, url
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Handle both empty object {} and object with sub-capabilities
|
||||
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
|
||||
self.form = try container.decodeIfPresent(Form.self, forKey: .form)
|
||||
self.url = try container.decodeIfPresent(URL.self, forKey: .url)
|
||||
// If both are nil, default to form for backward compatibility
|
||||
if self.form == nil && self.url == nil {
|
||||
self.form = Form()
|
||||
}
|
||||
} else {
|
||||
// Empty object - default to form-only for backward compatibility
|
||||
self.form = Form()
|
||||
self.url = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(form, forKey: .form)
|
||||
try container.encodeIfPresent(url, forKey: .url)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
233
Sources/MCP/Client/Elicitation.swift
Normal file
233
Sources/MCP/Client/Elicitation.swift
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import Foundation
|
||||
|
||||
/// Types supporting the MCP elicitation flow.
|
||||
///
|
||||
/// Servers use elicitation to collect structured input from users via the client.
|
||||
/// The schema subset mirrors the 2025-11-25 revision of the specification.
|
||||
public enum Elicitation {
|
||||
/// Schema describing the expected response content.
|
||||
public struct RequestSchema: Hashable, Codable, Sendable {
|
||||
/// Supported top-level types. Currently limited to objects.
|
||||
public enum SchemaType: String, Hashable, Codable, Sendable {
|
||||
case object
|
||||
}
|
||||
|
||||
/// Schema title presented to users.
|
||||
public var title: String?
|
||||
/// Schema description providing additional guidance.
|
||||
public var description: String?
|
||||
/// Raw JSON Schema fragments describing the requested fields.
|
||||
public var properties: [String: Value]
|
||||
/// List of required field keys.
|
||||
public var required: [String]?
|
||||
/// Top-level schema type. Defaults to `object`.
|
||||
public var type: SchemaType
|
||||
|
||||
public init(
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
properties: [String: Value] = [:],
|
||||
required: [String]? = nil,
|
||||
type: SchemaType = .object
|
||||
) {
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.properties = properties
|
||||
self.required = required
|
||||
self.type = type
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case title, description, properties, required, type
|
||||
}
|
||||
}
|
||||
|
||||
/// Elicitation mode indicating how user input is collected
|
||||
public enum Mode: String, Hashable, Codable, Sendable {
|
||||
/// Form-based elicitation (client displays UI)
|
||||
case form
|
||||
/// URL-based elicitation (client opens external URL)
|
||||
case url
|
||||
}
|
||||
}
|
||||
|
||||
/// To request information from a user, servers send an `elicitation/create` request.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation
|
||||
public enum CreateElicitation: Method {
|
||||
public static let name = "elicitation/create"
|
||||
|
||||
public enum Parameters: Hashable, Sendable {
|
||||
/// Form-based elicitation parameters
|
||||
case form(FormParameters)
|
||||
/// URL-based elicitation parameters
|
||||
case url(URLParameters)
|
||||
|
||||
/// Parameters for form-based elicitation
|
||||
public struct FormParameters: Hashable, Codable, Sendable {
|
||||
/// Message displayed to the user describing the request
|
||||
public var message: String
|
||||
/// Elicitation mode (optional for backward compatibility, defaults to form)
|
||||
public var mode: Elicitation.Mode?
|
||||
/// Optional schema describing the expected response content
|
||||
public var requestedSchema: Elicitation.RequestSchema?
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
message: String,
|
||||
mode: Elicitation.Mode? = nil,
|
||||
requestedSchema: Elicitation.RequestSchema? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.message = message
|
||||
self.mode = mode
|
||||
self.requestedSchema = requestedSchema
|
||||
self._meta = _meta
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for URL-based elicitation
|
||||
public struct URLParameters: Hashable, Codable, Sendable {
|
||||
/// Message displayed to the user describing the request
|
||||
public var message: String
|
||||
/// Elicitation mode (always "url")
|
||||
public var mode: Elicitation.Mode
|
||||
/// URL for the user to visit
|
||||
public var url: String
|
||||
/// Unique identifier for this elicitation
|
||||
public var elicitationId: String
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
message: String,
|
||||
url: String,
|
||||
elicitationId: String,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.message = message
|
||||
self.mode = .url
|
||||
self.url = url
|
||||
self.elicitationId = elicitationId
|
||||
self._meta = _meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
/// Indicates how the user responded to the request.
|
||||
public enum Action: String, Hashable, Codable, Sendable {
|
||||
case accept
|
||||
case decline
|
||||
case cancel
|
||||
}
|
||||
|
||||
/// Selected action.
|
||||
public var action: Action
|
||||
/// Submitted content when action is `.accept`.
|
||||
public var content: [String: Value]?
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
action: Action,
|
||||
content: [String: Value]? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.action = action
|
||||
self.content = content
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case action, content, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(action, forKey: .action)
|
||||
try container.encodeIfPresent(content, forKey: .content)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
action = try container.decode(Action.self, forKey: .action)
|
||||
content = try container.decodeIfPresent([String: Value].self, forKey: .content)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
extension CreateElicitation.Parameters: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case mode, message, requestedSchema, url, elicitationId
|
||||
case _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Read mode field (may be missing for backward compatibility)
|
||||
let mode = try container.decodeIfPresent(Elicitation.Mode.self, forKey: .mode)
|
||||
|
||||
// Discriminate based on mode
|
||||
if mode == .url {
|
||||
// URL mode
|
||||
let message = try container.decode(String.self, forKey: .message)
|
||||
let url = try container.decode(String.self, forKey: .url)
|
||||
let elicitationId = try container.decode(String.self, forKey: .elicitationId)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .url(URLParameters(
|
||||
message: message,
|
||||
url: url,
|
||||
elicitationId: elicitationId,
|
||||
_meta: _meta))
|
||||
} else {
|
||||
// Form mode (default for backward compatibility)
|
||||
let message = try container.decode(String.self, forKey: .message)
|
||||
let requestedSchema = try container.decodeIfPresent(
|
||||
Elicitation.RequestSchema.self, forKey: .requestedSchema)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .form(FormParameters(
|
||||
message: message,
|
||||
mode: mode,
|
||||
requestedSchema: requestedSchema,
|
||||
_meta: _meta))
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .form(let params):
|
||||
try container.encode(params.message, forKey: .message)
|
||||
try container.encodeIfPresent(params.mode, forKey: .mode)
|
||||
try container.encodeIfPresent(params.requestedSchema, forKey: .requestedSchema)
|
||||
try container.encodeIfPresent(params._meta, forKey: ._meta)
|
||||
case .url(let params):
|
||||
try container.encode(params.message, forKey: .message)
|
||||
try container.encode(params.mode, forKey: .mode)
|
||||
try container.encode(params.url, forKey: .url)
|
||||
try container.encode(params.elicitationId, forKey: .elicitationId)
|
||||
try container.encodeIfPresent(params._meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Notification sent when a URL-based elicitation is complete
|
||||
public struct ElicitationCompleteNotification: Notification {
|
||||
public static let name = "notifications/elicitation/complete"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
/// The elicitation ID that was completed
|
||||
public var elicitationId: String
|
||||
|
||||
public init(elicitationId: String) {
|
||||
self.elicitationId = elicitationId
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Sources/MCP/Client/Roots.swift
Normal file
93
Sources/MCP/Client/Roots.swift
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import Foundation
|
||||
|
||||
/// The Model Context Protocol (MCP) provides a mechanism for clients to expose
|
||||
/// filesystem boundaries to servers through roots. Roots allow servers to understand
|
||||
/// the scope of filesystem access they can request, enabling safe and controlled
|
||||
/// file operations.
|
||||
///
|
||||
/// Unlike Resources/Tools/Prompts, Roots use bidirectional communication:
|
||||
/// - Servers send `roots/list` requests TO clients
|
||||
/// - Clients respond with available roots
|
||||
/// - Clients send `notifications/roots/list_changed` when roots change
|
||||
///
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/client/roots
|
||||
public struct Root: Hashable, Codable, Sendable {
|
||||
/// The root URI (must use file:// scheme)
|
||||
public let uri: String
|
||||
/// Optional human-readable name for the root
|
||||
public let name: String?
|
||||
|
||||
public init(
|
||||
uri: String,
|
||||
name: String? = nil
|
||||
) {
|
||||
self.uri = uri
|
||||
self.name = name
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uri
|
||||
case name
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
uri = try container.decode(String.self, forKey: .uri)
|
||||
name = try container.decodeIfPresent(String.self, forKey: .name)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encodeIfPresent(name, forKey: .name)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// To discover available roots, servers send a `roots/list` request to the client.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/client/roots
|
||||
public enum ListRoots: Method {
|
||||
public static let name: String = "roots/list"
|
||||
|
||||
public typealias Parameters = Empty
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let roots: [Root]
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
roots: [Root],
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.roots = roots
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case roots, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(roots, forKey: .roots)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
roots = try container.decode([Root].self, forKey: .roots)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When the list of roots changes, clients that declared the `roots` capability
|
||||
/// SHOULD send this notification to inform servers.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/client/roots
|
||||
public struct RootsListChangedNotification: Notification {
|
||||
public static let name: String = "notifications/roots/list_changed"
|
||||
|
||||
public typealias Parameters = Empty
|
||||
}
|
||||
|
|
@ -48,10 +48,55 @@ public enum Sampling {
|
|||
|
||||
/// Content types for sampling messages
|
||||
public enum Content: Hashable, Sendable {
|
||||
/// Text content
|
||||
case text(String)
|
||||
/// Image content
|
||||
case image(data: String, mimeType: String)
|
||||
/// Single content block
|
||||
case single(ContentBlock)
|
||||
/// Multiple content blocks
|
||||
case multiple([ContentBlock])
|
||||
|
||||
/// Individual content blocks in messages
|
||||
public enum ContentBlock: Hashable, Sendable {
|
||||
/// Text content
|
||||
case text(String)
|
||||
/// Image content
|
||||
case image(data: String, mimeType: String)
|
||||
/// Audio content
|
||||
case audio(data: String, mimeType: String)
|
||||
/// Tool use content
|
||||
case toolUse(Sampling.ToolUseContent)
|
||||
/// Tool result content
|
||||
case toolResult(Sampling.ToolResultContent)
|
||||
}
|
||||
|
||||
/// Returns true if this is a single content block
|
||||
public var isSingle: Bool {
|
||||
if case .single = self { return true }
|
||||
return false
|
||||
}
|
||||
|
||||
/// Returns content as an array of blocks
|
||||
public var asArray: [ContentBlock] {
|
||||
switch self {
|
||||
case .single(let block):
|
||||
return [block]
|
||||
case .multiple(let blocks):
|
||||
return blocks
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates content from a text string (convenience)
|
||||
public static func text(_ text: String) -> Content {
|
||||
.single(.text(text))
|
||||
}
|
||||
|
||||
/// Creates content from an image (convenience)
|
||||
public static func image(data: String, mimeType: String) -> Content {
|
||||
.single(.image(data: data, mimeType: mimeType))
|
||||
}
|
||||
|
||||
/// Creates content from audio (convenience)
|
||||
public static func audio(data: String, mimeType: String) -> Content {
|
||||
.single(.audio(data: data, mimeType: mimeType))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -107,14 +152,79 @@ public enum Sampling {
|
|||
case stopSequence
|
||||
/// Reached maximum tokens
|
||||
case maxTokens
|
||||
/// Model wants to use a tool
|
||||
case toolUse
|
||||
}
|
||||
|
||||
/// Content representing a tool use request from the model
|
||||
public struct ToolUseContent: Hashable, Codable, Sendable {
|
||||
/// Unique identifier for this tool use
|
||||
public let id: String
|
||||
/// Name of the tool being invoked
|
||||
public let name: String
|
||||
/// Input parameters for the tool
|
||||
public let input: [String: Value]
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(id: String, name: String, input: [String: Value], _meta: Metadata? = nil) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.input = input
|
||||
self._meta = _meta
|
||||
}
|
||||
}
|
||||
|
||||
/// Content representing the result of a tool execution
|
||||
public struct ToolResultContent: Hashable, Codable, Sendable {
|
||||
/// ID of the tool use this result corresponds to
|
||||
public let toolUseId: String
|
||||
/// Content blocks from tool execution
|
||||
public let content: [ContentBlock]
|
||||
/// Structured data from tool execution
|
||||
public let structuredContent: [String: Value]?
|
||||
/// Whether the tool execution resulted in an error
|
||||
public let isError: Bool?
|
||||
/// Optional metadata
|
||||
public var _meta: Metadata?
|
||||
|
||||
/// Individual content blocks in tool results
|
||||
public enum ContentBlock: Hashable, Sendable {
|
||||
/// Text content
|
||||
case text(String)
|
||||
/// Image content
|
||||
case image(data: String, mimeType: String)
|
||||
/// Audio content
|
||||
case audio(data: String, mimeType: String)
|
||||
/// Embedded resource content
|
||||
case resource(resource: Resource.Content, annotations: Resource.Annotations?, _meta: Metadata?)
|
||||
/// Resource link
|
||||
case resourceLink(uri: String, name: String, title: String?, description: String?, mimeType: String?, annotations: Resource.Annotations?)
|
||||
}
|
||||
|
||||
public init(
|
||||
toolUseId: String,
|
||||
content: [ContentBlock],
|
||||
structuredContent: [String: Value]? = nil,
|
||||
isError: Bool? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.toolUseId = toolUseId
|
||||
self.content = content
|
||||
self.structuredContent = structuredContent
|
||||
self.isError = isError
|
||||
self._meta = _meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
extension Sampling.Message.Content: Codable {
|
||||
extension Sampling.Message.Content.ContentBlock: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, text, data, mimeType
|
||||
case id, name, input, _meta
|
||||
case toolUseId, content, structuredContent, isError
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -129,10 +239,32 @@ extension Sampling.Message.Content: Codable {
|
|||
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 "audio":
|
||||
let data = try container.decode(String.self, forKey: .data)
|
||||
let mimeType = try container.decode(String.self, forKey: .mimeType)
|
||||
self = .audio(data: data, mimeType: mimeType)
|
||||
case "toolUse":
|
||||
let id = try container.decode(String.self, forKey: .id)
|
||||
let name = try container.decode(String.self, forKey: .name)
|
||||
let input = try container.decode([String: Value].self, forKey: .input)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .toolUse(Sampling.ToolUseContent(id: id, name: name, input: input, _meta: _meta))
|
||||
case "toolResult":
|
||||
let toolUseId = try container.decode(String.self, forKey: .toolUseId)
|
||||
let content = try container.decode([Sampling.ToolResultContent.ContentBlock].self, forKey: .content)
|
||||
let structuredContent = try container.decodeIfPresent([String: Value].self, forKey: .structuredContent)
|
||||
let isError = try container.decodeIfPresent(Bool.self, forKey: .isError)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .toolResult(Sampling.ToolResultContent(
|
||||
toolUseId: toolUseId,
|
||||
content: content,
|
||||
structuredContent: structuredContent,
|
||||
isError: isError,
|
||||
_meta: _meta))
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type, in: container,
|
||||
debugDescription: "Unknown sampling message content type")
|
||||
debugDescription: "Unknown sampling message content block type")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +279,52 @@ extension Sampling.Message.Content: Codable {
|
|||
try container.encode("image", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
case .audio(let data, let mimeType):
|
||||
try container.encode("audio", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
case .toolUse(let toolUse):
|
||||
try container.encode("toolUse", forKey: .type)
|
||||
try container.encode(toolUse.id, forKey: .id)
|
||||
try container.encode(toolUse.name, forKey: .name)
|
||||
try container.encode(toolUse.input, forKey: .input)
|
||||
try container.encodeIfPresent(toolUse._meta, forKey: ._meta)
|
||||
case .toolResult(let toolResult):
|
||||
try container.encode("toolResult", forKey: .type)
|
||||
try container.encode(toolResult.toolUseId, forKey: .toolUseId)
|
||||
try container.encode(toolResult.content, forKey: .content)
|
||||
try container.encodeIfPresent(toolResult.structuredContent, forKey: .structuredContent)
|
||||
try container.encodeIfPresent(toolResult.isError, forKey: .isError)
|
||||
try container.encodeIfPresent(toolResult._meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Sampling.Message.Content: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Try to decode as an array first
|
||||
if let blocks = try? [ContentBlock](from: decoder) {
|
||||
// If it's a single-element array, unwrap it to single
|
||||
if blocks.count == 1, let block = blocks.first {
|
||||
self = .single(block)
|
||||
} else {
|
||||
self = .multiple(blocks)
|
||||
}
|
||||
} else {
|
||||
// Try to decode as a single block
|
||||
let block = try ContentBlock(from: decoder)
|
||||
self = .single(block)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .single(let block):
|
||||
// Encode single block directly (not as array)
|
||||
try block.encode(to: encoder)
|
||||
case .multiple(let blocks):
|
||||
// Encode as array
|
||||
try blocks.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +333,7 @@ extension Sampling.Message.Content: Codable {
|
|||
|
||||
extension Sampling.Message.Content: ExpressibleByStringLiteral {
|
||||
public init(stringLiteral value: String) {
|
||||
self = .text(value)
|
||||
self = .single(.text(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,7 +341,83 @@ extension Sampling.Message.Content: ExpressibleByStringLiteral {
|
|||
|
||||
extension Sampling.Message.Content: ExpressibleByStringInterpolation {
|
||||
public init(stringInterpolation: DefaultStringInterpolation) {
|
||||
self = .text(String(stringInterpolation: stringInterpolation))
|
||||
self = .single(.text(String(stringInterpolation: stringInterpolation)))
|
||||
}
|
||||
}
|
||||
|
||||
extension Sampling.ToolResultContent.ContentBlock: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, text, data, mimeType, resource, annotations, _meta
|
||||
case uri, name, title, description
|
||||
}
|
||||
|
||||
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)
|
||||
self = .image(data: data, mimeType: mimeType)
|
||||
case "audio":
|
||||
let data = try container.decode(String.self, forKey: .data)
|
||||
let mimeType = try container.decode(String.self, forKey: .mimeType)
|
||||
self = .audio(data: data, mimeType: mimeType)
|
||||
case "resource":
|
||||
let resourceContent = try container.decode(Resource.Content.self, forKey: .resource)
|
||||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .resource(resource: resourceContent, annotations: annotations, _meta: _meta)
|
||||
case "resourceLink":
|
||||
let uri = try container.decode(String.self, forKey: .uri)
|
||||
let name = try container.decode(String.self, forKey: .name)
|
||||
let title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
let description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
let mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType)
|
||||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
self = .resourceLink(
|
||||
uri: uri, name: name, title: title, description: description,
|
||||
mimeType: mimeType, annotations: annotations)
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type, in: container,
|
||||
debugDescription: "Unknown tool result content block 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):
|
||||
try container.encode("image", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
case .audio(let data, let mimeType):
|
||||
try container.encode("audio", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
case .resource(let resourceContent, let annotations, let _meta):
|
||||
try container.encode("resource", forKey: .type)
|
||||
try container.encode(resourceContent, forKey: .resource)
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
case .resourceLink(let uri, let name, let title, let description, let mimeType, let annotations):
|
||||
try container.encode("resourceLink", forKey: .type)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(description, forKey: .description)
|
||||
try container.encodeIfPresent(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,6 +428,26 @@ extension Sampling.Message.Content: ExpressibleByStringInterpolation {
|
|||
public enum CreateSamplingMessage: Method {
|
||||
public static let name = "sampling/createMessage"
|
||||
|
||||
/// Tool choice configuration for sampling
|
||||
public struct ToolChoice: Hashable, Codable, Sendable {
|
||||
/// Tool choice mode
|
||||
public enum Mode: String, Hashable, Codable, Sendable {
|
||||
/// Automatically decide whether to use tools
|
||||
case auto
|
||||
/// Require using at least one tool
|
||||
case required
|
||||
/// Do not use any tools
|
||||
case none
|
||||
}
|
||||
|
||||
/// The tool choice mode
|
||||
public let mode: Mode
|
||||
|
||||
public init(mode: Mode) {
|
||||
self.mode = mode
|
||||
}
|
||||
}
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
/// The conversation history to send to the LLM
|
||||
public let messages: [Sampling.Message]
|
||||
|
|
@ -189,8 +463,12 @@ public enum CreateSamplingMessage: Method {
|
|||
public let maxTokens: Int
|
||||
/// Array of sequences that stop generation
|
||||
public let stopSequences: [String]?
|
||||
/// Additional provider-specific parameters
|
||||
public let metadata: [String: Value]?
|
||||
/// Optional request metadata
|
||||
public var _meta: Metadata?
|
||||
/// Tools available for the model to use
|
||||
public let tools: [Tool]?
|
||||
/// Tool choice configuration
|
||||
public let toolChoice: ToolChoice?
|
||||
|
||||
public init(
|
||||
messages: [Sampling.Message],
|
||||
|
|
@ -200,7 +478,9 @@ public enum CreateSamplingMessage: Method {
|
|||
temperature: Double? = nil,
|
||||
maxTokens: Int,
|
||||
stopSequences: [String]? = nil,
|
||||
metadata: [String: Value]? = nil
|
||||
_meta: Metadata? = nil,
|
||||
tools: [Tool]? = nil,
|
||||
toolChoice: ToolChoice? = nil
|
||||
) {
|
||||
self.messages = messages
|
||||
self.modelPreferences = modelPreferences
|
||||
|
|
@ -209,7 +489,9 @@ public enum CreateSamplingMessage: Method {
|
|||
self.temperature = temperature
|
||||
self.maxTokens = maxTokens
|
||||
self.stopSequences = stopSequences
|
||||
self.metadata = metadata
|
||||
self._meta = _meta
|
||||
self.tools = tools
|
||||
self.toolChoice = toolChoice
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,17 +504,44 @@ public enum CreateSamplingMessage: Method {
|
|||
public let role: Sampling.Message.Role
|
||||
/// The completion content
|
||||
public let content: Sampling.Message.Content
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
model: String,
|
||||
stopReason: Sampling.StopReason? = nil,
|
||||
role: Sampling.Message.Role,
|
||||
content: Sampling.Message.Content
|
||||
content: Sampling.Message.Content,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.model = model
|
||||
self.stopReason = stopReason
|
||||
self.role = role
|
||||
self.content = content
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case model, stopReason, role, content, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(model, forKey: .model)
|
||||
try container.encodeIfPresent(stopReason, forKey: .stopReason)
|
||||
try container.encode(role, forKey: .role)
|
||||
try container.encode(content, forKey: .content)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
model = try container.decode(String.self, forKey: .model)
|
||||
stopReason = try container.decodeIfPresent(
|
||||
Sampling.StopReason.self, forKey: .stopReason)
|
||||
role = try container.decode(Sampling.Message.Role.self, forKey: .role)
|
||||
content = try container.decode(Sampling.Message.Content.self, forKey: .content)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue