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
6 changed files with 11854 additions and 828 deletions
192
Sources/MCP/Server/Completion.swift
Normal file
192
Sources/MCP/Server/Completion.swift
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
import Foundation
|
||||
|
||||
/// The Model Context Protocol (MCP) provides a standardized way for servers to offer
|
||||
/// autocompletion suggestions for the arguments of prompts and resource templates.
|
||||
///
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion/
|
||||
|
||||
// MARK: - Reference Types
|
||||
|
||||
/// A reference to a prompt by name
|
||||
public struct PromptReference: Hashable, Codable, Sendable {
|
||||
/// The prompt 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)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
guard type == "ref/prompt" else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type,
|
||||
in: container,
|
||||
debugDescription: "Expected ref/prompt type"
|
||||
)
|
||||
}
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a resource by URI
|
||||
public struct ResourceReference: Hashable, Codable, Sendable {
|
||||
/// The resource URI
|
||||
public let uri: String
|
||||
|
||||
public init(uri: String) {
|
||||
self.uri = uri
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, uri
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode("ref/resource", forKey: .type)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
guard type == "ref/resource" else {
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type,
|
||||
in: container,
|
||||
debugDescription: "Expected ref/resource type"
|
||||
)
|
||||
}
|
||||
uri = try container.decode(String.self, forKey: .uri)
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference type for completion requests (either prompt or resource)
|
||||
public enum CompletionReference: Hashable, Codable, Sendable {
|
||||
/// References a prompt by name
|
||||
case prompt(PromptReference)
|
||||
/// References a resource URI
|
||||
case resource(ResourceReference)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
}
|
||||
|
||||
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 "ref/prompt":
|
||||
self = .prompt(try PromptReference(from: decoder))
|
||||
case "ref/resource":
|
||||
self = .resource(try ResourceReference(from: decoder))
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type,
|
||||
in: container,
|
||||
debugDescription: "Unknown reference type: \(type)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
switch self {
|
||||
case .prompt(let ref):
|
||||
try ref.encode(to: encoder)
|
||||
case .resource(let ref):
|
||||
try ref.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Completion Request
|
||||
|
||||
/// To get completion suggestions, clients send a `completion/complete` request.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion/
|
||||
public enum Complete: Method {
|
||||
public static let name = "completion/complete"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
/// The reference to what is being completed
|
||||
public let ref: CompletionReference
|
||||
/// The argument being completed
|
||||
public let argument: Argument
|
||||
/// Optional context with already-resolved arguments
|
||||
public let context: Context?
|
||||
|
||||
public init(
|
||||
ref: CompletionReference,
|
||||
argument: Argument,
|
||||
context: Context? = nil
|
||||
) {
|
||||
self.ref = ref
|
||||
self.argument = argument
|
||||
self.context = context
|
||||
}
|
||||
|
||||
/// The argument being completed
|
||||
public struct Argument: Hashable, Codable, Sendable {
|
||||
/// The argument name
|
||||
public let name: String
|
||||
/// The current value (partial or complete)
|
||||
public let value: String
|
||||
|
||||
public init(name: String, value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
/// Context containing already-resolved arguments
|
||||
public struct Context: Hashable, Codable, Sendable {
|
||||
/// A mapping of already-resolved argument names to their values
|
||||
public let arguments: [String: Value]
|
||||
|
||||
public init(arguments: [String: Value]) {
|
||||
self.arguments = arguments
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
/// The completion result
|
||||
public let completion: Completion
|
||||
|
||||
public init(completion: Completion) {
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
/// Completion result containing suggested values
|
||||
public struct Completion: Hashable, Codable, Sendable {
|
||||
/// Array of completion values (max 100 items)
|
||||
public let values: [String]
|
||||
/// Optional total number of available matches
|
||||
public let total: Int?
|
||||
/// Whether additional results exist
|
||||
public let hasMore: Bool?
|
||||
|
||||
public init(
|
||||
values: [String],
|
||||
total: Int? = nil,
|
||||
hasMore: Bool? = nil
|
||||
) {
|
||||
self.values = values
|
||||
self.total = total
|
||||
self.hasMore = hasMore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
Sources/MCP/Server/Logging.swift
Normal file
72
Sources/MCP/Server/Logging.swift
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import Foundation
|
||||
|
||||
/// The Model Context Protocol (MCP) provides a standardized way for servers to send
|
||||
/// structured log messages to clients. Clients can control logging verbosity by setting
|
||||
/// minimum log levels, with servers sending notifications containing severity levels,
|
||||
/// optional logger names, and arbitrary JSON-serializable data.
|
||||
///
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging/
|
||||
public enum LogLevel: String, Hashable, Codable, Sendable, CaseIterable {
|
||||
/// Detailed debugging information
|
||||
case debug
|
||||
/// General informational messages
|
||||
case info
|
||||
/// Normal but significant events
|
||||
case notice
|
||||
/// Warning conditions
|
||||
case warning
|
||||
/// Error conditions
|
||||
case error
|
||||
/// Critical conditions
|
||||
case critical
|
||||
/// Action must be taken immediately
|
||||
case alert
|
||||
/// System is unusable
|
||||
case emergency
|
||||
}
|
||||
|
||||
// MARK: - Set Log Level
|
||||
|
||||
/// To configure the minimum log level, clients MAY send a `logging/setLevel` request.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging/
|
||||
public enum SetLoggingLevel: Method {
|
||||
public static let name = "logging/setLevel"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
/// The minimum log level to set
|
||||
public let level: LogLevel
|
||||
|
||||
public init(level: LogLevel) {
|
||||
self.level = level
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Result = Empty
|
||||
}
|
||||
|
||||
// MARK: - Log Message Notification
|
||||
|
||||
/// Servers send log messages using `notifications/message` notifications.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging/
|
||||
public struct LogMessageNotification: Notification {
|
||||
public static let name = "notifications/message"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
/// The severity level of the log message
|
||||
public let level: LogLevel
|
||||
/// Optional logger name to identify the source
|
||||
public let logger: String?
|
||||
/// Arbitrary JSON-serializable data for the log message
|
||||
public let data: Value
|
||||
|
||||
public init(
|
||||
level: LogLevel,
|
||||
logger: String? = nil,
|
||||
data: Value
|
||||
) {
|
||||
self.level = level
|
||||
self.logger = logger
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,32 +7,80 @@ import Foundation
|
|||
/// 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/
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts/
|
||||
public struct Prompt: Hashable, Codable, Sendable {
|
||||
/// The prompt name
|
||||
public let name: String
|
||||
/// A human-readable prompt title
|
||||
public let title: String?
|
||||
/// The prompt description
|
||||
public let description: String?
|
||||
/// The prompt arguments
|
||||
public let arguments: [Argument]?
|
||||
/// Optional set of sized icons that the client can display in a user interface
|
||||
public var icons: [Icon]?
|
||||
/// Optional metadata about this prompt
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(name: String, description: String? = nil, arguments: [Argument]? = nil) {
|
||||
public init(
|
||||
name: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
arguments: [Argument]? = nil,
|
||||
icons: [Icon]? = nil,
|
||||
meta: Metadata? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.arguments = arguments
|
||||
self.icons = icons
|
||||
self._meta = meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name, title, description, arguments, icons, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(description, forKey: .description)
|
||||
try container.encodeIfPresent(arguments, forKey: .arguments)
|
||||
try container.encodeIfPresent(icons, forKey: . icons)
|
||||
try container.encodeIfPresent(_meta, forKey: . _meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
arguments = try container.decodeIfPresent([Argument].self, forKey: .arguments)
|
||||
icons = try container.decodeIfPresent([Icon].self, forKey: . icons)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: . _meta)
|
||||
}
|
||||
|
||||
/// An argument for a prompt
|
||||
public struct Argument: Hashable, Codable, Sendable {
|
||||
/// The argument name
|
||||
public let name: String
|
||||
/// A human-readable argument title
|
||||
public let title: 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) {
|
||||
public init(
|
||||
name: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
required: Bool? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.required = required
|
||||
}
|
||||
|
|
@ -86,8 +134,8 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
case image(data: String, mimeType: String)
|
||||
/// Audio content
|
||||
case audio(data: String, mimeType: String)
|
||||
/// Embedded resource content
|
||||
case resource(uri: String, mimeType: String, text: String?, blob: String?)
|
||||
/// Embedded resource content (EmbeddedResource from spec)
|
||||
case resource(resource: Resource.Content, annotations: Resource.Annotations? = nil, _meta: Metadata? = nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -95,25 +143,30 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
public struct Reference: Hashable, Codable, Sendable {
|
||||
/// The prompt reference name
|
||||
public let name: String
|
||||
/// A human-readable prompt title
|
||||
public let title: String?
|
||||
|
||||
public init(name: String) {
|
||||
public init(name: String, title: String? = nil) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, name
|
||||
case type, name, title
|
||||
}
|
||||
|
||||
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)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
}
|
||||
|
||||
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)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -122,7 +175,7 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
|
||||
extension Prompt.Message.Content: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, text, data, mimeType, uri, blob
|
||||
case type, text, data, mimeType, resource, annotations, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -140,12 +193,11 @@ extension Prompt.Message.Content: Codable {
|
|||
try container.encode("audio", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
case .resource(let uri, let mimeType, let text, let blob):
|
||||
case .resource(let resourceContent, let annotations, let _meta):
|
||||
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)
|
||||
try container.encode(resourceContent, forKey: .resource)
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,11 +218,10 @@ extension Prompt.Message.Content: Codable {
|
|||
let mimeType = try container.decode(String.self, forKey: .mimeType)
|
||||
self = .audio(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)
|
||||
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)
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type,
|
||||
|
|
@ -199,7 +250,7 @@ extension Prompt.Message.Content: ExpressibleByStringInterpolation {
|
|||
// MARK: -
|
||||
|
||||
/// To retrieve available prompts, clients send a `prompts/list` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#listing-prompts
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2024-11-05/server/prompts/#listing-prompts
|
||||
public enum ListPrompts: Method {
|
||||
public static let name: String = "prompts/list"
|
||||
|
||||
|
|
@ -216,12 +267,36 @@ public enum ListPrompts: Method {
|
|||
}
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let prompts: [Prompt]
|
||||
public let nextCursor: String?
|
||||
let prompts: [Prompt]
|
||||
let nextCursor: String?
|
||||
var _meta: Metadata?
|
||||
|
||||
public init(prompts: [Prompt], nextCursor: String? = nil) {
|
||||
public init(
|
||||
prompts: [Prompt],
|
||||
nextCursor: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.prompts = prompts
|
||||
self.nextCursor = nextCursor
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case prompts, nextCursor, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(prompts, forKey: .prompts)
|
||||
try container.encodeIfPresent(nextCursor, forKey: .nextCursor)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
prompts = try container.decode([Prompt].self, forKey: .prompts)
|
||||
nextCursor = try container.decodeIfPresent(String.self, forKey: .nextCursor)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -245,10 +320,35 @@ public enum GetPrompt: Method {
|
|||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let description: String?
|
||||
public let messages: [Prompt.Message]
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(description: String?, messages: [Prompt.Message]) {
|
||||
public init(
|
||||
description: String? = nil,
|
||||
messages: [Prompt.Message],
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.description = description
|
||||
self.messages = messages
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case description, messages, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(description, forKey: .description)
|
||||
try container.encode(messages, forKey: .messages)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
messages = try container.decode([Prompt.Message].self, forKey: .messages)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,10 +6,12 @@ import Foundation
|
|||
/// 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/
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/
|
||||
public struct Resource: Hashable, Codable, Sendable {
|
||||
/// The resource name
|
||||
public var name: String
|
||||
/// A human-readable resource title
|
||||
public var title: String?
|
||||
/// The resource URI
|
||||
public var uri: String
|
||||
/// The resource description
|
||||
|
|
@ -18,19 +20,64 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
public var mimeType: String?
|
||||
/// The resource metadata
|
||||
public var metadata: [String: String]?
|
||||
/// Optional set of sized icons that the client can display in a user interface
|
||||
public var icons: [Icon]?
|
||||
/// Metadata fields for the resource (see spec for _meta usage)
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
name: String,
|
||||
uri: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
mimeType: String? = nil,
|
||||
metadata: [String: String]? = nil
|
||||
metadata: [String: String]? = nil,
|
||||
icons: [Icon]? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.uri = uri
|
||||
self.description = description
|
||||
self.mimeType = mimeType
|
||||
self.metadata = metadata
|
||||
self.icons = icons
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case uri
|
||||
case title
|
||||
case description
|
||||
case mimeType
|
||||
case metadata
|
||||
case icons
|
||||
case _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
uri = try container.decode(String.self, forKey: .uri)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType)
|
||||
metadata = try container.decodeIfPresent([String: String].self, forKey: .metadata)
|
||||
icons = try container.decodeIfPresent([Icon].self, forKey: .icons)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(description, forKey: .description)
|
||||
try container.encodeIfPresent(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(metadata, forKey: .metadata)
|
||||
try container.encodeIfPresent(icons, forKey: .icons)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
/// Content of a resource.
|
||||
|
|
@ -43,27 +90,82 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
public let text: String?
|
||||
/// The resource binary content
|
||||
public let blob: String?
|
||||
/// Metadata fields (see spec for _meta usage)
|
||||
public var _meta: Metadata?
|
||||
|
||||
public static func text(_ content: String, uri: String, mimeType: String? = nil) -> Self {
|
||||
.init(uri: uri, mimeType: mimeType, text: content)
|
||||
public static func text(
|
||||
_ content: String,
|
||||
uri: String,
|
||||
mimeType: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) -> Self {
|
||||
.init(uri: uri, mimeType: mimeType, text: content, _meta: _meta)
|
||||
}
|
||||
|
||||
public static func binary(_ data: Data, uri: String, mimeType: String? = nil) -> Self {
|
||||
.init(uri: uri, mimeType: mimeType, blob: data.base64EncodedString())
|
||||
public static func binary(
|
||||
_ data: Data,
|
||||
uri: String,
|
||||
mimeType: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) -> Self {
|
||||
.init(
|
||||
uri: uri,
|
||||
mimeType: mimeType,
|
||||
blob: data.base64EncodedString(),
|
||||
_meta: _meta
|
||||
)
|
||||
}
|
||||
|
||||
private init(uri: String, mimeType: String? = nil, text: String? = nil) {
|
||||
private init(
|
||||
uri: String,
|
||||
mimeType: String? = nil,
|
||||
text: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.uri = uri
|
||||
self.mimeType = mimeType
|
||||
self.text = text
|
||||
self.blob = nil
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private init(uri: String, mimeType: String? = nil, blob: String) {
|
||||
private init(
|
||||
uri: String,
|
||||
mimeType: String? = nil,
|
||||
blob: String,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.uri = uri
|
||||
self.mimeType = mimeType
|
||||
self.text = nil
|
||||
self.blob = blob
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uri
|
||||
case mimeType
|
||||
case text
|
||||
case blob
|
||||
case _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
uri = try container.decode(String.self, forKey: .uri)
|
||||
mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType)
|
||||
text = try container.decodeIfPresent(String.self, forKey: .text)
|
||||
blob = try container.decodeIfPresent(String.self, forKey: .blob)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encodeIfPresent(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(text, forKey: .text)
|
||||
try container.encodeIfPresent(blob, forKey: .blob)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,21 +175,57 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
public var uriTemplate: String
|
||||
/// The template name
|
||||
public var name: String
|
||||
/// A human-readable template title
|
||||
public var title: String?
|
||||
/// The template description
|
||||
public var description: String?
|
||||
/// The resource MIME type
|
||||
public var mimeType: String?
|
||||
/// Optional set of sized icons that the client can display in a user interface
|
||||
public var icons: [Icon]?
|
||||
|
||||
public init(
|
||||
uriTemplate: String,
|
||||
name: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
mimeType: String? = nil
|
||||
mimeType: String? = nil,
|
||||
icons: [Icon]? = nil
|
||||
) {
|
||||
self.uriTemplate = uriTemplate
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.mimeType = mimeType
|
||||
self.icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
// A resource annotation.
|
||||
public struct Annotations: Hashable, Codable, Sendable {
|
||||
/// The intended audience for this resource.
|
||||
public enum Audience: String, Hashable, Codable, Sendable {
|
||||
/// Content intended for end users.
|
||||
case user = "user"
|
||||
/// Content intended for AI assistants.
|
||||
case assistant = "assistant"
|
||||
}
|
||||
|
||||
/// An array indicating the intended audience(s) for this resource. For example, `[.user, .assistant]` indicates content useful for both.
|
||||
public let audience: [Audience]?
|
||||
/// A number from 0.0 to 1.0 indicating the importance of this resource. A value of 1 means "most important" (effectively required), while 0 means "least important".
|
||||
public let priority: Double?
|
||||
/// An ISO 8601 formatted timestamp indicating when the resource was last modified (e.g., "2025-01-12T15:00:58Z").
|
||||
public let lastModified: String?
|
||||
|
||||
public init(
|
||||
audience: [Audience]? = nil,
|
||||
priority: Double? = nil,
|
||||
lastModified: String? = nil
|
||||
) {
|
||||
self.audience = audience
|
||||
self.priority = priority
|
||||
self.lastModified = lastModified
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +233,7 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
// MARK: -
|
||||
|
||||
/// To discover available resources, clients send a `resources/list` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#listing-resources
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#listing-resources
|
||||
public enum ListResources: Method {
|
||||
public static let name: String = "resources/list"
|
||||
|
||||
|
|
@ -105,7 +243,7 @@ public enum ListResources: Method {
|
|||
public init() {
|
||||
self.cursor = nil
|
||||
}
|
||||
|
||||
|
||||
public init(cursor: String) {
|
||||
self.cursor = cursor
|
||||
}
|
||||
|
|
@ -114,16 +252,40 @@ public enum ListResources: Method {
|
|||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let resources: [Resource]
|
||||
public let nextCursor: String?
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(resources: [Resource], nextCursor: String? = nil) {
|
||||
public init(
|
||||
resources: [Resource],
|
||||
nextCursor: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.resources = resources
|
||||
self.nextCursor = nextCursor
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case resources, nextCursor, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(resources, forKey: .resources)
|
||||
try container.encodeIfPresent(nextCursor, forKey: .nextCursor)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
resources = try container.decode([Resource].self, forKey: .resources)
|
||||
nextCursor = try container.decodeIfPresent(String.self, forKey: .nextCursor)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// To retrieve resource contents, clients send a `resources/read` request:
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#reading-resources
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#reading-resources
|
||||
public enum ReadResource: Method {
|
||||
public static let name: String = "resources/read"
|
||||
|
||||
|
|
@ -137,15 +299,37 @@ public enum ReadResource: Method {
|
|||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let contents: [Resource.Content]
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(contents: [Resource.Content]) {
|
||||
public init(
|
||||
contents: [Resource.Content],
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.contents = contents
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case contents, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(contents, forKey: .contents)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
contents = try container.decode([Resource.Content].self, forKey: .contents)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#resource-templates
|
||||
public enum ListResourceTemplates: Method {
|
||||
public static let name: String = "resources/templates/list"
|
||||
|
||||
|
|
@ -155,7 +339,7 @@ public enum ListResourceTemplates: Method {
|
|||
public init() {
|
||||
self.cursor = nil
|
||||
}
|
||||
|
||||
|
||||
public init(cursor: String) {
|
||||
self.cursor = cursor
|
||||
}
|
||||
|
|
@ -164,21 +348,43 @@ public enum ListResourceTemplates: Method {
|
|||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let templates: [Resource.Template]
|
||||
public let nextCursor: String?
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(templates: [Resource.Template], nextCursor: String? = nil) {
|
||||
public init(
|
||||
templates: [Resource.Template],
|
||||
nextCursor: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.templates = templates
|
||||
self.nextCursor = nextCursor
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case templates = "resourceTemplates"
|
||||
case nextCursor
|
||||
case _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(templates, forKey: .templates)
|
||||
try container.encodeIfPresent(nextCursor, forKey: .nextCursor)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
templates = try container.decode([Resource.Template].self, forKey: .templates)
|
||||
nextCursor = try container.decodeIfPresent(String.self, forKey: .nextCursor)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#list-changed-notification
|
||||
public struct ResourceListChangedNotification: Notification {
|
||||
public static let name: String = "notifications/resources/list_changed"
|
||||
|
||||
|
|
@ -186,7 +392,7 @@ public struct ResourceListChangedNotification: Notification {
|
|||
}
|
||||
|
||||
/// Clients can subscribe to specific resources and receive notifications when they change.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/resources/#subscriptions
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#subscriptions
|
||||
public enum ResourceSubscribe: Method {
|
||||
public static let name: String = "resources/subscribe"
|
||||
|
||||
|
|
@ -197,8 +403,20 @@ public enum ResourceSubscribe: Method {
|
|||
public typealias Result = Empty
|
||||
}
|
||||
|
||||
/// Sent from the client to request cancellation of resources/updated notifications from the server. This should follow a previous resources/subscribe request.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-06-18/schema#unsubscriberequest
|
||||
public enum ResourceUnsubscribe: Method {
|
||||
public static let name: String = "resources/unsubscribe"
|
||||
|
||||
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
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#subscriptions
|
||||
public struct ResourceUpdatedNotification: Notification {
|
||||
public static let name: String = "notifications/resources/updated"
|
||||
|
||||
|
|
|
|||
|
|
@ -30,11 +30,14 @@ public actor Server {
|
|||
public struct Info: Hashable, Codable, Sendable {
|
||||
/// The server name
|
||||
public let name: String
|
||||
/// A human-readable server title for display
|
||||
public let title: String?
|
||||
/// The server version
|
||||
public let 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
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +90,13 @@ public actor Server {
|
|||
public init() {}
|
||||
}
|
||||
|
||||
/// Completions capabilities
|
||||
public struct Completions: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// Completions capabilities
|
||||
public var completions: Completions?
|
||||
/// Logging capabilities
|
||||
public var logging: Logging?
|
||||
/// Prompts capabilities
|
||||
|
|
@ -99,12 +109,14 @@ public actor Server {
|
|||
public var tools: Tools?
|
||||
|
||||
public init(
|
||||
completions: Completions? = nil,
|
||||
logging: Logging? = nil,
|
||||
prompts: Prompts? = nil,
|
||||
resources: Resources? = nil,
|
||||
sampling: Sampling? = nil,
|
||||
tools: Tools? = nil
|
||||
) {
|
||||
self.completions = completions
|
||||
self.logging = logging
|
||||
self.prompts = prompts
|
||||
self.resources = resources
|
||||
|
|
@ -126,25 +138,72 @@ public actor Server {
|
|||
|
||||
/// The server name
|
||||
public nonisolated var name: String { serverInfo.name }
|
||||
/// A human-readable server title
|
||||
public nonisolated var title: String? { serverInfo.title }
|
||||
/// The server version
|
||||
public nonisolated var version: String { serverInfo.version }
|
||||
/// Instructions describing how to use the server and its features
|
||||
///
|
||||
/// This can be used by clients to improve the LLM's understanding of
|
||||
/// available tools, resources, etc.
|
||||
/// It can be thought of like a "hint" to the model.
|
||||
/// This can be used by clients to improve the LLM's understanding of
|
||||
/// available tools, resources, etc.
|
||||
/// It can be thought of like a "hint" to the model.
|
||||
/// For example, this information MAY be added to the system prompt.
|
||||
public nonisolated let instructions: String?
|
||||
/// The server capabilities
|
||||
public var capabilities: Capabilities
|
||||
/// The server configuration
|
||||
public var configuration: Configuration
|
||||
|
||||
|
||||
/// Request handlers
|
||||
private var methodHandlers: [String: RequestHandlerBox] = [:]
|
||||
/// Notification handlers
|
||||
private var notificationHandlers: [String: [NotificationHandlerBox]] = [:]
|
||||
/// Pending request tasks (for cancellation support)
|
||||
private var pendingRequestTasks: [ID: Task<Response<AnyMethod>, Error>] = [:]
|
||||
|
||||
/// An error indicating a type mismatch when decoding a pending request
|
||||
private struct TypeMismatchError: Swift.Error {}
|
||||
|
||||
/// A pending request with a continuation for the result
|
||||
private struct PendingRequest<T> {
|
||||
let continuation: CheckedContinuation<T, Swift.Error>
|
||||
}
|
||||
|
||||
/// A type-erased pending request
|
||||
private struct AnyPendingRequest: Sendable {
|
||||
private let _resume: @Sendable (Result<Any, Swift.Error>) -> Void
|
||||
|
||||
init<T: Sendable & Decodable>(_ request: PendingRequest<T>) {
|
||||
_resume = { result in
|
||||
switch result {
|
||||
case .success(let value):
|
||||
if let typedValue = value as? T {
|
||||
request.continuation.resume(returning: typedValue)
|
||||
} else if let value = value as? Value,
|
||||
let data = try? JSONEncoder().encode(value),
|
||||
let decoded = try? JSONDecoder().decode(T.self, from: data)
|
||||
{
|
||||
request.continuation.resume(returning: decoded)
|
||||
} else {
|
||||
request.continuation.resume(throwing: TypeMismatchError())
|
||||
}
|
||||
case .failure(let error):
|
||||
request.continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resume(returning value: Any) {
|
||||
_resume(.success(value))
|
||||
}
|
||||
|
||||
func resume(throwing error: Swift.Error) {
|
||||
_resume(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
/// Pending requests sent to the client, awaiting responses
|
||||
private var pendingRequests: [ID: AnyPendingRequest] = [:]
|
||||
|
||||
/// Whether the server is initialized
|
||||
private var isInitialized = false
|
||||
|
|
@ -162,11 +221,12 @@ public actor Server {
|
|||
public init(
|
||||
name: String,
|
||||
version: String,
|
||||
title: String? = nil,
|
||||
instructions: String? = nil,
|
||||
capabilities: Server.Capabilities = .init(),
|
||||
configuration: Configuration = .default
|
||||
) {
|
||||
self.serverInfo = Server.Info(name: name, version: version)
|
||||
self.serverInfo = Server.Info(name: name, version: version, title: title)
|
||||
self.capabilities = capabilities
|
||||
self.configuration = configuration
|
||||
self.instructions = instructions
|
||||
|
|
@ -182,10 +242,12 @@ public actor Server {
|
|||
) async throws {
|
||||
self.connection = transport
|
||||
registerDefaultHandlers(initializeHook: initializeHook)
|
||||
registerCancellationHandler()
|
||||
try await transport.connect()
|
||||
|
||||
await logger?.debug(
|
||||
"Server started", metadata: ["name": "\(name)", "version": "\(version)"])
|
||||
"Server started", metadata: ["name": "\(name)", "version": "\(version)"]
|
||||
)
|
||||
|
||||
// Start message handling loop
|
||||
task = Task {
|
||||
|
|
@ -196,12 +258,17 @@ public actor Server {
|
|||
|
||||
var requestID: ID?
|
||||
do {
|
||||
// Attempt to decode as batch first, then as individual request or notification
|
||||
// Attempt to decode as batch first, then as individual response, request, or notification
|
||||
let decoder = JSONDecoder()
|
||||
if let batch = try? decoder.decode(Server.Batch.self, from: data) {
|
||||
try await handleBatch(batch)
|
||||
} 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) {
|
||||
_ = try await handleRequest(request, sendResponse: true)
|
||||
// Handle request in a separate task to avoid blocking the receive loop
|
||||
Task {
|
||||
_ = try? await self.handleRequest(request, sendResponse: true)
|
||||
}
|
||||
} else if let message = try? decoder.decode(AnyMessage.self, from: data) {
|
||||
try await handleMessage(message)
|
||||
} else {
|
||||
|
|
@ -245,6 +312,14 @@ public actor Server {
|
|||
public func stop() async {
|
||||
task?.cancel()
|
||||
task = nil
|
||||
|
||||
// Clear pending requests with errors
|
||||
let pendingRequestsToCancel = self.pendingRequests
|
||||
self.pendingRequests = [:]
|
||||
for (_, request) in pendingRequestsToCancel {
|
||||
request.resume(throwing: MCPError.internalError("Server disconnected"))
|
||||
}
|
||||
|
||||
if let connection = connection {
|
||||
await connection.disconnect()
|
||||
}
|
||||
|
|
@ -309,6 +384,62 @@ public actor Server {
|
|||
try await connection.send(notificationData)
|
||||
}
|
||||
|
||||
/// Send a request to the client and return a Task for the response
|
||||
private func send<M: Method>(_ request: Request<M>) throws -> Task<M.Result, Error> {
|
||||
guard let connection = connection else {
|
||||
throw MCPError.internalError("Server connection not initialized")
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
|
||||
let requestData = try encoder.encode(request)
|
||||
|
||||
let requestTask = Task<M.Result, Error> {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
Task {
|
||||
// Add pending response before sending
|
||||
self.addPendingResponse(
|
||||
id: request.id,
|
||||
continuation: continuation,
|
||||
type: M.Result.self
|
||||
)
|
||||
|
||||
// Send the request
|
||||
do {
|
||||
try await connection.send(requestData)
|
||||
} catch {
|
||||
// If send fails, remove pending response and resume with error
|
||||
if self.removePendingResponse(id: request.id) != nil {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return requestTask
|
||||
}
|
||||
|
||||
/// Send a request and await its response
|
||||
private func sendAndAwait<M: Method>(_ request: Request<M>) async throws -> M.Result {
|
||||
let task = try send(request)
|
||||
return try await task.value
|
||||
}
|
||||
|
||||
private func addPendingResponse<T: Sendable & Decodable>(
|
||||
id: ID,
|
||||
continuation: CheckedContinuation<T, Swift.Error>,
|
||||
type: T.Type
|
||||
) {
|
||||
pendingRequests[id] = AnyPendingRequest(
|
||||
PendingRequest(continuation: continuation)
|
||||
)
|
||||
}
|
||||
|
||||
private func removePendingResponse(id: ID) -> AnyPendingRequest? {
|
||||
return pendingRequests.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
// MARK: - Sampling
|
||||
|
||||
/// Request sampling from the connected client
|
||||
|
|
@ -331,7 +462,7 @@ public actor Server {
|
|||
/// - temperature: Controls randomness (0.0 to 1.0)
|
||||
/// - maxTokens: Maximum tokens to generate
|
||||
/// - stopSequences: Array of sequences that stop generation
|
||||
/// - metadata: Additional provider-specific parameters
|
||||
/// - _meta: Optional request metadata
|
||||
/// - Returns: The sampling result containing the model used, stop reason, role, and content
|
||||
/// - Throws: MCPError if the request fails
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
|
||||
|
|
@ -343,17 +474,15 @@ public actor Server {
|
|||
temperature: Double? = nil,
|
||||
maxTokens: Int,
|
||||
stopSequences: [String]? = nil,
|
||||
metadata: [String: Value]? = nil
|
||||
_meta: Metadata? = nil
|
||||
) async throws -> CreateSamplingMessage.Result {
|
||||
guard connection != nil else {
|
||||
throw MCPError.internalError("Server connection not initialized")
|
||||
}
|
||||
|
||||
// Note: This is a conceptual implementation. The actual implementation would require
|
||||
// bidirectional communication support in the transport layer, allowing servers to
|
||||
// send requests to clients and receive responses.
|
||||
try validateClientCapability(\.sampling, "Sampling")
|
||||
|
||||
_ = CreateSamplingMessage.request(
|
||||
let request = CreateSamplingMessage.request(
|
||||
.init(
|
||||
messages: messages,
|
||||
modelPreferences: modelPreferences,
|
||||
|
|
@ -362,14 +491,168 @@ public actor Server {
|
|||
temperature: temperature,
|
||||
maxTokens: maxTokens,
|
||||
stopSequences: stopSequences,
|
||||
metadata: metadata
|
||||
_meta: _meta
|
||||
)
|
||||
)
|
||||
|
||||
// This would need to be implemented with proper request/response handling
|
||||
// similar to how the client sends requests to servers
|
||||
throw MCPError.internalError(
|
||||
"Bidirectional sampling requests not yet implemented in transport layer")
|
||||
let result = try await sendAndAwait(request)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Elicitation
|
||||
|
||||
/// Request user input from the client using form-based elicitation
|
||||
///
|
||||
/// Elicitation allows servers to request user input during operations.
|
||||
/// This is useful for collecting user feedback, confirmations, or data
|
||||
/// that the server needs but doesn't have.
|
||||
///
|
||||
/// The flow:
|
||||
/// 1. Server requests elicitation with a message and optional schema
|
||||
/// 2. Client displays the request to the user
|
||||
/// 3. User provides input or declines
|
||||
/// 4. Client returns the result to the server
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: The message to display to the user
|
||||
/// - mode: The elicitation mode (form or url)
|
||||
/// - requestedSchema: Optional JSON schema describing the expected response
|
||||
/// - _meta: Optional request metadata
|
||||
/// - Returns: The elicitation result containing the action and optional content
|
||||
/// - Throws: MCPError if the request fails
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/elicitation
|
||||
public func requestElicitation(
|
||||
message: String,
|
||||
mode: Elicitation.Mode? = nil,
|
||||
requestedSchema: Elicitation.RequestSchema? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) async throws -> CreateElicitation.Result {
|
||||
guard connection != nil else {
|
||||
throw MCPError.internalError("Server connection not initialized")
|
||||
}
|
||||
|
||||
try validateClientCapability(\.elicitation, "Elicitation")
|
||||
|
||||
let request = CreateElicitation.request(
|
||||
.form(
|
||||
.init(
|
||||
message: message,
|
||||
mode: mode,
|
||||
requestedSchema: requestedSchema,
|
||||
_meta: _meta
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let result = try await sendAndAwait(request)
|
||||
return result
|
||||
}
|
||||
|
||||
/// Request user input from the client using URL-based elicitation
|
||||
///
|
||||
/// URL-based elicitation directs the user to an external URL for authentication
|
||||
/// or data collection. This is useful for OAuth flows or other web-based input.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - message: The message to display to the user
|
||||
/// - url: The URL to direct the user to
|
||||
/// - elicitationId: Unique identifier for this elicitation
|
||||
/// - _meta: Optional request metadata
|
||||
/// - Returns: The elicitation result containing the action and optional content
|
||||
/// - Throws: MCPError if the request fails
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/elicitation
|
||||
public func requestElicitation(
|
||||
message: String,
|
||||
url: String,
|
||||
elicitationId: String,
|
||||
_meta: Metadata? = nil
|
||||
) async throws -> CreateElicitation.Result {
|
||||
guard connection != nil else {
|
||||
throw MCPError.internalError("Server connection not initialized")
|
||||
}
|
||||
|
||||
try validateClientCapability(\.elicitation, "Elicitation")
|
||||
|
||||
let request = CreateElicitation.request(
|
||||
.url(
|
||||
.init(
|
||||
message: message,
|
||||
url: url,
|
||||
elicitationId: elicitationId,
|
||||
_meta: _meta
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
let result = try await sendAndAwait(request)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Logging
|
||||
|
||||
/// Send a log message notification to connected clients.
|
||||
///
|
||||
/// Servers that declare the `logging` capability can send structured log messages
|
||||
/// to clients. The client controls which severity levels it wants to receive via
|
||||
/// the `logging/setLevel` request.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - level: The severity level of the log message
|
||||
/// - logger: Optional logger name to identify the source
|
||||
/// - data: Arbitrary JSON-serializable data for the log message
|
||||
/// - Throws: MCPError if the server is not connected
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging/
|
||||
public func log(
|
||||
level: LogLevel,
|
||||
logger: String? = nil,
|
||||
data: Value
|
||||
) async throws {
|
||||
let notification = LogMessageNotification.message(
|
||||
.init(level: level, logger: logger, data: data)
|
||||
)
|
||||
try await notify(notification)
|
||||
}
|
||||
|
||||
/// Send a log message notification with codable data.
|
||||
///
|
||||
/// Convenience method that encodes data to JSON before sending.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - level: The severity level of the log message
|
||||
/// - logger: Optional logger name to identify the source
|
||||
/// - data: Any codable data for the log message
|
||||
/// - Throws: MCPError if the server is not connected or encoding fails
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/logging/
|
||||
public func log<T: Codable>(
|
||||
level: LogLevel,
|
||||
logger: String? = nil,
|
||||
data: T
|
||||
) async throws {
|
||||
let value = try Value(data)
|
||||
try await log(level: level, logger: logger, data: value)
|
||||
}
|
||||
|
||||
// MARK: - Roots
|
||||
|
||||
/// Request the list of roots from the connected client
|
||||
///
|
||||
/// Roots define filesystem boundaries that servers can operate within.
|
||||
/// The client must have declared the `roots` capability and registered
|
||||
/// a roots handler for this to work.
|
||||
///
|
||||
/// - Returns: Array of Root objects representing accessible directories/files
|
||||
/// - Throws: MCPError if the client doesn't support roots or request fails
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/client/roots
|
||||
public func listRoots() async throws -> [Root] {
|
||||
guard connection != nil else {
|
||||
throw MCPError.internalError("Server connection not initialized")
|
||||
}
|
||||
|
||||
try validateClientCapability(\.roots, "Roots")
|
||||
|
||||
let request = ListRoots.request()
|
||||
let result = try await sendAndAwait(request)
|
||||
return result.roots
|
||||
}
|
||||
|
||||
/// A JSON-RPC batch containing multiple requests and/or notifications
|
||||
|
|
@ -491,9 +774,39 @@ public actor Server {
|
|||
return response
|
||||
}
|
||||
|
||||
// Create a task to handle the request with cancellation support
|
||||
let handlerTask = Task<Response<AnyMethod>, Error> {
|
||||
do {
|
||||
// Check if task was cancelled before starting
|
||||
try Task.checkCancellation()
|
||||
|
||||
// Handle request and get response
|
||||
let response = try await handler(request)
|
||||
return response
|
||||
} catch is CancellationError {
|
||||
// Request was cancelled, don't send a response per MCP spec
|
||||
await logger?.debug(
|
||||
"Request cancelled",
|
||||
metadata: ["id": "\(request.id)", "method": "\(request.method)"]
|
||||
)
|
||||
throw CancellationError()
|
||||
} catch {
|
||||
let mcpError =
|
||||
error as? MCPError ?? MCPError.internalError(error.localizedDescription)
|
||||
return AnyMethod.response(id: request.id, error: mcpError)
|
||||
}
|
||||
}
|
||||
|
||||
// Store the handler task for potential cancellation
|
||||
pendingRequestTasks[request.id] = handlerTask
|
||||
|
||||
// Ensure cleanup happens regardless of success or failure
|
||||
defer {
|
||||
pendingRequestTasks.removeValue(forKey: request.id)
|
||||
}
|
||||
|
||||
do {
|
||||
// Handle request and get response
|
||||
let response = try await handler(request)
|
||||
let response = try await handlerTask.value
|
||||
|
||||
if sendResponse {
|
||||
try await send(response)
|
||||
|
|
@ -501,7 +814,11 @@ public actor Server {
|
|||
}
|
||||
|
||||
return response
|
||||
} catch is CancellationError {
|
||||
// Request was cancelled, don't send a response per MCP spec
|
||||
return nil
|
||||
} catch {
|
||||
// This should not happen as errors are caught in the task
|
||||
let mcpError = error as? MCPError ?? MCPError.internalError(error.localizedDescription)
|
||||
let response = AnyMethod.response(id: request.id, error: mcpError)
|
||||
|
||||
|
|
@ -544,12 +861,46 @@ public actor Server {
|
|||
}
|
||||
}
|
||||
|
||||
private func handleResponse(_ response: Response<AnyMethod>) async {
|
||||
if let pendingRequest = self.removePendingResponse(id: response.id) {
|
||||
switch response.result {
|
||||
case .success(let value):
|
||||
pendingRequest.resume(returning: value)
|
||||
case .failure(let error):
|
||||
pendingRequest.resume(throwing: error)
|
||||
}
|
||||
} else {
|
||||
await logger?.warning(
|
||||
"Received response for unknown request",
|
||||
metadata: ["id": "\(response.id)"]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func checkInitialized() throws {
|
||||
guard isInitialized else {
|
||||
throw MCPError.invalidRequest("Server is not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate the client capabilities.
|
||||
/// Throws an error if the server is configured to be strict and the capability is not supported.
|
||||
private func validateClientCapability<T>(
|
||||
_ keyPath: KeyPath<Client.Capabilities, T?>,
|
||||
_ name: String
|
||||
)
|
||||
throws
|
||||
{
|
||||
if configuration.strict {
|
||||
guard let capabilities = clientCapabilities else {
|
||||
throw MCPError.methodNotFound("Client capabilities not initialized")
|
||||
}
|
||||
guard capabilities[keyPath: keyPath] != nil else {
|
||||
throw MCPError.methodNotFound("\(name) is not supported by the client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerDefaultHandlers(
|
||||
initializeHook: (@Sendable (Client.Info, Client.Capabilities) async throws -> Void)?
|
||||
) {
|
||||
|
|
@ -602,6 +953,68 @@ public actor Server {
|
|||
self.protocolVersion = protocolVersion
|
||||
self.isInitialized = true
|
||||
}
|
||||
|
||||
/// Cancel and remove a pending request task
|
||||
private func removePendingRequest(id: ID) -> Task<Response<AnyMethod>, Error>? {
|
||||
pendingRequestTasks.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
private func registerCancellationHandler() {
|
||||
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",
|
||||
]
|
||||
)
|
||||
|
||||
// Cancel the pending request task if it exists and remove from tracking
|
||||
if let task = await self.removePendingRequest(id: requestId) {
|
||||
task.cancel()
|
||||
await self.logger?.debug(
|
||||
"Cancelled request",
|
||||
metadata: ["requestId": "\(requestId)"]
|
||||
)
|
||||
} else {
|
||||
// Request may have already completed or is unknown
|
||||
// Per MCP spec, we should ignore this gracefully
|
||||
await self.logger?.trace(
|
||||
"Cancellation notification for unknown or completed request",
|
||||
metadata: ["requestId": "\(requestId)"]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel a request by sending a CancelledNotification to the client.
|
||||
///
|
||||
/// This is used when the server needs to cancel an in-progress request it made to the client
|
||||
/// (e.g., a sampling request).
|
||||
///
|
||||
/// According to the MCP specification, cancellation is advisory:
|
||||
/// - The client SHOULD stop processing and free resources
|
||||
/// - The client MAY ignore the cancellation if the request is unknown, already completed,
|
||||
/// or cannot be cancelled
|
||||
/// - The server SHOULD ignore any response that arrives after cancellation
|
||||
///
|
||||
/// - 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 {
|
||||
// Send cancellation notification to client
|
||||
let notification = CancelledNotification.message(
|
||||
.init(requestId: requestID, reason: reason)
|
||||
)
|
||||
try await notify(notification)
|
||||
}
|
||||
}
|
||||
|
||||
extension Server.Batch: Codable {
|
||||
|
|
|
|||
|
|
@ -7,14 +7,22 @@ import Foundation
|
|||
/// 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/
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/tools/
|
||||
public struct Tool: Hashable, Codable, Sendable {
|
||||
/// The tool name
|
||||
public let name: String
|
||||
/// The human-readable name of the tool for display purposes.
|
||||
public let title: String?
|
||||
/// The tool description
|
||||
public let description: String?
|
||||
/// The tool input schema
|
||||
public let inputSchema: Value
|
||||
/// Optional set of sized icons that the client can display in a user interface
|
||||
public var icons: [Icon]?
|
||||
/// The tool output schema, defining expected output structure
|
||||
public let outputSchema: Value?
|
||||
/// Metadata fields for the tool (see spec for _meta usage)
|
||||
public var _meta: Metadata?
|
||||
|
||||
/// Annotations that provide display-facing and operational information for a Tool.
|
||||
///
|
||||
|
|
@ -82,17 +90,25 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
/// Annotations that provide display-facing and operational information
|
||||
public var annotations: Annotations
|
||||
|
||||
/// Initialize a tool with a name, description, input schema, and annotations
|
||||
/// Initialize a tool with a name, description, input schema, annotations, and optional icons
|
||||
public init(
|
||||
name: String,
|
||||
title: String? = nil,
|
||||
description: String?,
|
||||
inputSchema: Value,
|
||||
annotations: Annotations = nil
|
||||
annotations: Annotations = nil,
|
||||
outputSchema: Value? = nil,
|
||||
icons: [Icon]? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.inputSchema = inputSchema
|
||||
self.outputSchema = outputSchema
|
||||
self.annotations = annotations
|
||||
self._meta = _meta
|
||||
self.icons = icons
|
||||
}
|
||||
|
||||
/// Content types that can be returned by a tool
|
||||
|
|
@ -100,22 +116,33 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
/// Text content
|
||||
case text(String)
|
||||
/// Image content
|
||||
case image(data: String, mimeType: String, metadata: [String: String]?)
|
||||
case image(data: String, mimeType: String, metadata: Metadata?)
|
||||
/// Audio content
|
||||
case audio(data: String, mimeType: String)
|
||||
/// Embedded resource content
|
||||
case resource(uri: String, mimeType: String, text: String?)
|
||||
/// Embedded resource content (EmbeddedResource from spec)
|
||||
case resource(resource: Resource.Content, annotations: Resource.Annotations? = nil, _meta: Metadata? = nil)
|
||||
/// Resource link
|
||||
case resourceLink(
|
||||
uri: String, name: String, title: String? = nil, description: String? = nil,
|
||||
mimeType: String? = nil,
|
||||
annotations: Resource.Annotations? = nil
|
||||
)
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case text
|
||||
case image
|
||||
case resource
|
||||
case resourceLink
|
||||
case audio
|
||||
case uri
|
||||
case name
|
||||
case title
|
||||
case description
|
||||
case annotations
|
||||
case mimeType
|
||||
case data
|
||||
case metadata
|
||||
case _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -129,18 +156,29 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
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)
|
||||
let _meta = try container.decodeIfPresent(
|
||||
Metadata.self, forKey: ._meta)
|
||||
self = .image(data: data, mimeType: mimeType, metadata: _meta)
|
||||
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 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)
|
||||
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 content type")
|
||||
|
|
@ -158,51 +196,72 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
try container.encode("image", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(metadata, forKey: .metadata)
|
||||
try container.encodeIfPresent(metadata, forKey: ._meta)
|
||||
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 uri, let mimeType, let text):
|
||||
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(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(text, forKey: .text)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case name
|
||||
case title
|
||||
case description
|
||||
case inputSchema
|
||||
case outputSchema
|
||||
case annotations
|
||||
case icons
|
||||
case _meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
inputSchema = try container.decode(Value.self, forKey: .inputSchema)
|
||||
outputSchema = try container.decodeIfPresent(Value.self, forKey: .outputSchema)
|
||||
annotations =
|
||||
try container.decodeIfPresent(Tool.Annotations.self, forKey: .annotations) ?? .init()
|
||||
icons = try container.decodeIfPresent([Icon].self, forKey: .icons)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encode(description, forKey: .description)
|
||||
try container.encode(inputSchema, forKey: .inputSchema)
|
||||
try container.encodeIfPresent(outputSchema, forKey: .outputSchema)
|
||||
if !annotations.isEmpty {
|
||||
try container.encode(annotations, forKey: .annotations)
|
||||
}
|
||||
try container.encodeIfPresent(icons, forKey: .icons)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
/// To discover available tools, clients send a `tools/list` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#listing-tools
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#listing-tools
|
||||
public enum ListTools: Method {
|
||||
public static let name = "tools/list"
|
||||
|
||||
|
|
@ -221,42 +280,142 @@ public enum ListTools: Method {
|
|||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let tools: [Tool]
|
||||
public let nextCursor: String?
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(tools: [Tool], nextCursor: String? = nil) {
|
||||
public init(
|
||||
tools: [Tool],
|
||||
nextCursor: String? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.tools = tools
|
||||
self.nextCursor = nextCursor
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case tools, nextCursor, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(tools, forKey: .tools)
|
||||
try container.encodeIfPresent(nextCursor, forKey: .nextCursor)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
tools = try container.decode([Tool].self, forKey: .tools)
|
||||
nextCursor = try container.decodeIfPresent(String.self, forKey: .nextCursor)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// To call a tool, clients send a `tools/call` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#calling-tools
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/tools/#calling-tools
|
||||
public enum CallTool: Method {
|
||||
public static let name = "tools/call"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
/// Optional request metadata including progress token.
|
||||
///
|
||||
/// If `progressToken` is specified, the caller is requesting out-of-band
|
||||
/// progress notifications for this request.
|
||||
public let _meta: Metadata?
|
||||
|
||||
/// The name of the tool to call.
|
||||
public let name: String
|
||||
|
||||
/// Arguments to use for the tool call.
|
||||
public let arguments: [String: Value]?
|
||||
|
||||
public init(name: String, arguments: [String: Value]? = nil) {
|
||||
public init(name: String, arguments: [String: Value]? = nil, meta: Metadata? = nil) {
|
||||
self._meta = meta
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case _meta
|
||||
case name
|
||||
case arguments
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
name = try container.decode(String.self, forKey: .name)
|
||||
arguments = try container.decodeIfPresent([String: Value].self, forKey: .arguments)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(arguments, forKey: .arguments)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
public let content: [Tool.Content]
|
||||
public let structuredContent: Value?
|
||||
public let isError: Bool?
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(content: [Tool.Content], isError: Bool? = nil) {
|
||||
public init(
|
||||
content: [Tool.Content] = [],
|
||||
structuredContent: Value? = nil,
|
||||
isError: Bool? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.content = content
|
||||
self.structuredContent = structuredContent
|
||||
self.isError = isError
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
public init<Output: Codable>(
|
||||
content: [Tool.Content] = [],
|
||||
structuredContent: Output,
|
||||
isError: Bool? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) throws {
|
||||
let encoded = try Value(structuredContent)
|
||||
self.init(
|
||||
content: content,
|
||||
structuredContent: Optional.some(encoded),
|
||||
isError: isError,
|
||||
_meta: _meta
|
||||
)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case content, structuredContent, isError, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(content, forKey: .content)
|
||||
try container.encodeIfPresent(structuredContent, forKey: .structuredContent)
|
||||
try container.encodeIfPresent(isError, forKey: .isError)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
content = try container.decode([Tool.Content].self, forKey: .content)
|
||||
structuredContent = try container.decodeIfPresent(
|
||||
Value.self, forKey: .structuredContent)
|
||||
isError = try container.decodeIfPresent(Bool.self, forKey: .isError)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#list-changed-notification
|
||||
public struct ToolListChangedNotification: Notification {
|
||||
public static let name: String = "notifications/tools/list_changed"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue