swift-sdk/Sources/MCP/Client/Elicitation.swift
yehorsobko 973b46b8f7
feat: 2025-11-25 review fixes (#201)
* fix: review fixes of the latest changes

* fix: updated unit tests

* chore: more conformance compliance improvements

* chore: reverted protocol changes

* chore: more compliance improvements

* chore: more conformance compliance improvements

* chore: improvements to the backcompat and updated to Tool unit tests

* chore: ci update
2026-03-03 17:33:28 +02:00

233 lines
8.7 KiB
Swift

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?
/// Schema describing the expected response content (required per spec)
public var requestedSchema: Elicitation.RequestSchema
/// Optional metadata
public var _meta: Metadata?
public init(
message: String,
mode: Elicitation.Mode? = nil,
requestedSchema: Elicitation.RequestSchema = .init(),
_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.decode(
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
}
}
}