swift-sdk/Sources/MCP/Client/Elicitation.swift
Maksym Mova 6112a3995a
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>
2026-02-19 13:09:31 +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?
/// 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
}
}
}