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
This commit is contained in:
parent
6112a3995a
commit
973b46b8f7
5 changed files with 692 additions and 507 deletions
|
|
@ -7,38 +7,10 @@ import Foundation
|
|||
|
||||
// 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 prompt by name.
|
||||
///
|
||||
/// This is a typealias for `Prompt.Reference` — the two types are equivalent.
|
||||
public typealias PromptReference = Prompt.Reference
|
||||
|
||||
/// A reference to a resource by URI
|
||||
public struct ResourceReference: Hashable, Codable, Sendable {
|
||||
|
|
@ -153,9 +125,9 @@ public enum Complete: Method {
|
|||
/// 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 let arguments: [String: String]
|
||||
|
||||
public init(arguments: [String: Value]) {
|
||||
public init(arguments: [String: String]) {
|
||||
self.arguments = arguments
|
||||
}
|
||||
}
|
||||
|
|
@ -164,9 +136,28 @@ public enum Complete: Method {
|
|||
public struct Result: Hashable, Codable, Sendable {
|
||||
/// The completion result
|
||||
public let completion: Completion
|
||||
/// Optional metadata about this result
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(completion: Completion) {
|
||||
public init(completion: Completion, _meta: Metadata? = nil) {
|
||||
self.completion = completion
|
||||
self._meta = _meta
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey, CaseIterable {
|
||||
case completion, _meta
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(completion, forKey: .completion)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
completion = try container.decode(Completion.self, forKey: .completion)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
/// Completion result containing suggested values
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
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)
|
||||
try container.encodeIfPresent(icons, forKey: .icons)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -58,8 +58,8 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
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)
|
||||
icons = try container.decodeIfPresent([Icon].self, forKey: .icons)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
||||
/// An argument for a prompt
|
||||
|
|
@ -136,6 +136,8 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
case audio(data: String, mimeType: 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,6 +178,7 @@ public struct Prompt: Hashable, Codable, Sendable {
|
|||
extension Prompt.Message.Content: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type, text, data, mimeType, resource, annotations, _meta
|
||||
case uri, name, title, description
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
|
|
@ -198,6 +201,14 @@ extension Prompt.Message.Content: Codable {
|
|||
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("resource_link", forKey: .type)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
try container.encodeIfPresent(description, forKey: .description)
|
||||
try container.encodeIfPresent(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,6 +233,14 @@ extension Prompt.Message.Content: Codable {
|
|||
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 "resource_link":
|
||||
let uri = try container.decode(String.self, forKey: .uri)
|
||||
let name = try container.decode(String.self, forKey: .name)
|
||||
let title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
let description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||
let mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType)
|
||||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
self = .resourceLink(uri: uri, name: name, title: title, description: description, mimeType: mimeType, annotations: annotations)
|
||||
default:
|
||||
throw DecodingError.dataCorruptedError(
|
||||
forKey: .type,
|
||||
|
|
@ -250,7 +269,7 @@ extension Prompt.Message.Content: ExpressibleByStringInterpolation {
|
|||
// MARK: -
|
||||
|
||||
/// To retrieve available prompts, clients send a `prompts/list` request.
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2024-11-05/server/prompts/#listing-prompts
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts/#listing-prompts
|
||||
public enum ListPrompts: Method {
|
||||
public static let name: String = "prompts/list"
|
||||
|
||||
|
|
@ -267,9 +286,9 @@ public enum ListPrompts: Method {
|
|||
}
|
||||
|
||||
public struct Result: Hashable, Codable, Sendable {
|
||||
let prompts: [Prompt]
|
||||
let nextCursor: String?
|
||||
var _meta: Metadata?
|
||||
public let prompts: [Prompt]
|
||||
public let nextCursor: String?
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
prompts: [Prompt],
|
||||
|
|
@ -303,15 +322,15 @@ public enum ListPrompts: Method {
|
|||
|
||||
/// To retrieve a specific prompt, clients send a `prompts/get` request.
|
||||
/// Arguments may be auto-completed through the completion API.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#getting-a-prompt
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts/#getting-a-prompt
|
||||
public enum GetPrompt: Method {
|
||||
public static let name: String = "prompts/get"
|
||||
|
||||
public struct Parameters: Hashable, Codable, Sendable {
|
||||
public let name: String
|
||||
public let arguments: [String: Value]?
|
||||
public let arguments: [String: String]?
|
||||
|
||||
public init(name: String, arguments: [String: Value]? = nil) {
|
||||
public init(name: String, arguments: [String: String]? = nil) {
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
}
|
||||
|
|
@ -354,7 +373,7 @@ public enum GetPrompt: Method {
|
|||
}
|
||||
|
||||
/// When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2024-11-05/server/prompts/#list-changed-notification
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/prompts/#list-changed-notification
|
||||
public struct PromptListChangedNotification: Notification {
|
||||
public static let name: String = "notifications/prompts/list_changed"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,10 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
public var description: String?
|
||||
/// The resource MIME type
|
||||
public var mimeType: String?
|
||||
/// The resource metadata
|
||||
public var metadata: [String: String]?
|
||||
/// Raw content size in bytes
|
||||
public var size: Int?
|
||||
/// Annotations for this resource
|
||||
public var annotations: Resource.Annotations?
|
||||
/// 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)
|
||||
|
|
@ -31,7 +33,8 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
mimeType: String? = nil,
|
||||
metadata: [String: String]? = nil,
|
||||
size: Int? = nil,
|
||||
annotations: Resource.Annotations? = nil,
|
||||
icons: [Icon]? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
|
|
@ -40,7 +43,8 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
self.uri = uri
|
||||
self.description = description
|
||||
self.mimeType = mimeType
|
||||
self.metadata = metadata
|
||||
self.size = size
|
||||
self.annotations = annotations
|
||||
self.icons = icons
|
||||
self._meta = _meta
|
||||
}
|
||||
|
|
@ -51,7 +55,8 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
case title
|
||||
case description
|
||||
case mimeType
|
||||
case metadata
|
||||
case size
|
||||
case annotations
|
||||
case icons
|
||||
case _meta
|
||||
}
|
||||
|
|
@ -63,7 +68,8 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
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)
|
||||
size = try container.decodeIfPresent(Int.self, forKey: .size)
|
||||
annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
icons = try container.decodeIfPresent([Icon].self, forKey: .icons)
|
||||
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
}
|
||||
|
|
@ -75,7 +81,8 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
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(size, forKey: .size)
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(icons, forKey: .icons)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
}
|
||||
|
|
@ -181,8 +188,12 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
public var description: String?
|
||||
/// The resource MIME type
|
||||
public var mimeType: String?
|
||||
/// Annotations for this template
|
||||
public var annotations: Resource.Annotations?
|
||||
/// Optional set of sized icons that the client can display in a user interface
|
||||
public var icons: [Icon]?
|
||||
/// Metadata fields for the template (see spec for _meta usage)
|
||||
public var _meta: Metadata?
|
||||
|
||||
public init(
|
||||
uriTemplate: String,
|
||||
|
|
@ -190,14 +201,18 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
mimeType: String? = nil,
|
||||
icons: [Icon]? = nil
|
||||
annotations: Resource.Annotations? = nil,
|
||||
icons: [Icon]? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) {
|
||||
self.uriTemplate = uriTemplate
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.mimeType = mimeType
|
||||
self.annotations = annotations
|
||||
self.icons = icons
|
||||
self._meta = _meta
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +248,7 @@ public struct Resource: Hashable, Codable, Sendable {
|
|||
// MARK: -
|
||||
|
||||
/// To discover available resources, clients send a `resources/list` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#listing-resources
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#listing-resources
|
||||
public enum ListResources: Method {
|
||||
public static let name: String = "resources/list"
|
||||
|
||||
|
|
@ -285,7 +300,7 @@ public enum ListResources: Method {
|
|||
}
|
||||
|
||||
/// To retrieve resource contents, clients send a `resources/read` request:
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#reading-resources
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#reading-resources
|
||||
public enum ReadResource: Method {
|
||||
public static let name: String = "resources/read"
|
||||
|
||||
|
|
@ -329,7 +344,7 @@ public enum ReadResource: Method {
|
|||
}
|
||||
|
||||
/// To discover available resource templates, clients send a `resources/templates/list` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#resource-templates
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#resource-templates
|
||||
public enum ListResourceTemplates: Method {
|
||||
public static let name: String = "resources/templates/list"
|
||||
|
||||
|
|
@ -384,7 +399,7 @@ public enum ListResourceTemplates: Method {
|
|||
}
|
||||
|
||||
/// When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#list-changed-notification
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#list-changed-notification
|
||||
public struct ResourceListChangedNotification: Notification {
|
||||
public static let name: String = "notifications/resources/list_changed"
|
||||
|
||||
|
|
@ -392,7 +407,7 @@ public struct ResourceListChangedNotification: Notification {
|
|||
}
|
||||
|
||||
/// Clients can subscribe to specific resources and receive notifications when they change.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#subscriptions
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#subscriptions
|
||||
public enum ResourceSubscribe: Method {
|
||||
public static let name: String = "resources/subscribe"
|
||||
|
||||
|
|
@ -416,7 +431,7 @@ public enum ResourceUnsubscribe: Method {
|
|||
}
|
||||
|
||||
/// When a resource changes, servers that declared the updated capability SHOULD send a notification to subscribed clients.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/resources/#subscriptions
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#subscriptions
|
||||
public struct ResourceUpdatedNotification: Notification {
|
||||
public static let name: String = "notifications/resources/updated"
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,27 @@ public actor Server {
|
|||
public let title: String?
|
||||
/// The server version
|
||||
public let version: String
|
||||
/// Optional description of the server
|
||||
public let description: String?
|
||||
/// Optional website URL for the server
|
||||
public let websiteUrl: String?
|
||||
/// Optional set of sized icons for display in a user interface
|
||||
public let icons: [Icon]?
|
||||
|
||||
public init(name: String, version: String, title: String? = nil) {
|
||||
public init(
|
||||
name: String,
|
||||
version: String,
|
||||
title: String? = nil,
|
||||
description: String? = nil,
|
||||
websiteUrl: String? = nil,
|
||||
icons: [Icon]? = nil
|
||||
) {
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.version = version
|
||||
self.description = description
|
||||
self.websiteUrl = websiteUrl
|
||||
self.icons = icons
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,11 +101,6 @@ public actor Server {
|
|||
public init() {}
|
||||
}
|
||||
|
||||
/// Sampling capabilities
|
||||
public struct Sampling: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
}
|
||||
|
||||
/// Completions capabilities
|
||||
public struct Completions: Hashable, Codable, Sendable {
|
||||
public init() {}
|
||||
|
|
@ -103,8 +114,6 @@ public actor Server {
|
|||
public var prompts: Prompts?
|
||||
/// Resources capabilities
|
||||
public var resources: Resources?
|
||||
/// Sampling capabilities
|
||||
public var sampling: Sampling?
|
||||
/// Tools capabilities
|
||||
public var tools: Tools?
|
||||
|
||||
|
|
@ -113,14 +122,12 @@ public actor Server {
|
|||
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
|
||||
self.sampling = sampling
|
||||
self.tools = tools
|
||||
}
|
||||
}
|
||||
|
|
@ -161,47 +168,6 @@ public actor Server {
|
|||
/// 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] = [:]
|
||||
|
||||
|
|
@ -330,6 +296,16 @@ public actor Server {
|
|||
await task?.value
|
||||
}
|
||||
|
||||
// MARK: - Request Context
|
||||
|
||||
/// The JSON-RPC request ID of the currently executing method handler.
|
||||
///
|
||||
/// Set via `@TaskLocal` before dispatching each request, so it propagates
|
||||
/// automatically into the handler task. Accessible package-wide for
|
||||
/// transports that need to identify the active request (e.g. closing an
|
||||
/// SSE stream mid-call for reconnection testing per SEP-1699).
|
||||
@TaskLocal package static var currentRequestID: ID? = nil
|
||||
|
||||
// MARK: - Registration
|
||||
|
||||
/// Register a method handler
|
||||
|
|
@ -523,8 +499,8 @@ public actor Server {
|
|||
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/elicitation
|
||||
public func requestElicitation(
|
||||
message: String,
|
||||
requestedSchema: Elicitation.RequestSchema,
|
||||
mode: Elicitation.Mode? = nil,
|
||||
requestedSchema: Elicitation.RequestSchema? = nil,
|
||||
_meta: Metadata? = nil
|
||||
) async throws -> CreateElicitation.Result {
|
||||
guard connection != nil else {
|
||||
|
|
@ -774,26 +750,30 @@ 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()
|
||||
// Create a task to handle the request with cancellation support.
|
||||
// Set currentRequestID as a task local so handlers can identify the active request.
|
||||
var handlerTask: Task<Response<AnyMethod>, Error>!
|
||||
Server.$currentRequestID.withValue(request.id) {
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -969,11 +949,19 @@ public actor Server {
|
|||
await self.logger?.debug(
|
||||
"Received cancellation notification",
|
||||
metadata: [
|
||||
"requestId": "\(requestId)",
|
||||
"requestId": requestId.map { "\($0)" } ?? "none",
|
||||
"reason": reason.map { "\($0)" } ?? "none",
|
||||
]
|
||||
)
|
||||
|
||||
guard let requestId = requestId else {
|
||||
await self.logger?.warning(
|
||||
"Received cancellation notification with no requestId (violates spec MUST)",
|
||||
metadata: ["reason": reason.map { "\($0)" } ?? "none"]
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel the pending request task if it exists and remove from tracking
|
||||
if let task = await self.removePendingRequest(id: requestId) {
|
||||
task.cancel()
|
||||
|
|
|
|||
|
|
@ -114,11 +114,11 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
/// Content types that can be returned by a tool
|
||||
public enum Content: Hashable, Codable, Sendable {
|
||||
/// Text content
|
||||
case text(String)
|
||||
case text(text: String, annotations: Resource.Annotations?, _meta: Metadata?)
|
||||
/// Image content
|
||||
case image(data: String, mimeType: String, metadata: Metadata?)
|
||||
case image(data: String, mimeType: String, annotations: Resource.Annotations?, _meta: Metadata?)
|
||||
/// Audio content
|
||||
case audio(data: String, mimeType: String)
|
||||
case audio(data: String, mimeType: String, annotations: Resource.Annotations?, _meta: Metadata?)
|
||||
/// Embedded resource content (EmbeddedResource from spec)
|
||||
case resource(resource: Resource.Content, annotations: Resource.Annotations? = nil, _meta: Metadata? = nil)
|
||||
/// Resource link
|
||||
|
|
@ -128,12 +128,48 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
annotations: Resource.Annotations? = nil
|
||||
)
|
||||
|
||||
/// Deprecated compatibility factory for older call sites that used `.text("...")` and `.text("...", metadata: ...)`.
|
||||
@available(*, deprecated, message: "Use .text(text:annotations:_meta:) instead.")
|
||||
public static func text(_ text: String, metadata: Metadata? = nil) -> Self {
|
||||
.text(text: text, annotations: nil, _meta: metadata)
|
||||
}
|
||||
|
||||
/// Deprecated compatibility factory for older call sites that used `.text(text: ..., metadata: ...)`.
|
||||
@available(*, deprecated, message: "Use .text(text:annotations:_meta:) instead.")
|
||||
public static func text(text: String, metadata: Metadata? = nil) -> Self {
|
||||
.text(text: text, annotations: nil, _meta: metadata)
|
||||
}
|
||||
|
||||
/// Deprecated compatibility factory for older call sites that used `.image(..., metadata: ...)`.
|
||||
@available(*, deprecated, message: "Use .image(data:mimeType:annotations:_meta:) instead.")
|
||||
public static func image(_ data: String, _ mimeType: String, metadata: Metadata? = nil) -> Self {
|
||||
.image(data: data, mimeType: mimeType, annotations: nil, _meta: metadata)
|
||||
}
|
||||
|
||||
/// Deprecated compatibility factory for older call sites that used `.image(data:mimeType:metadata:)`.
|
||||
@available(*, deprecated, message: "Use .image(data:mimeType:annotations:_meta:) instead.")
|
||||
public static func image(data: String, mimeType: String, metadata: Metadata? = nil) -> Self {
|
||||
.image(data: data, mimeType: mimeType, annotations: nil, _meta: metadata)
|
||||
}
|
||||
|
||||
/// Deprecated compatibility factory for older call sites that used `.audio(..., metadata: ...)`.
|
||||
@available(*, deprecated, message: "Use .audio(data:mimeType:annotations:_meta:) instead.")
|
||||
public static func audio(_ data: String, _ mimeType: String, metadata: Metadata? = nil) -> Self {
|
||||
.audio(data: data, mimeType: mimeType, annotations: nil, _meta: metadata)
|
||||
}
|
||||
|
||||
/// Deprecated compatibility factory for older call sites that used `.audio(data:mimeType:metadata:)`.
|
||||
@available(*, deprecated, message: "Use .audio(data:mimeType:annotations:_meta:) instead.")
|
||||
public static func audio(data: String, mimeType: String, metadata: Metadata? = nil) -> Self {
|
||||
.audio(data: data, mimeType: mimeType, annotations: nil, _meta: metadata)
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case text
|
||||
case image
|
||||
case resource
|
||||
case resourceLink
|
||||
case resource_link
|
||||
case audio
|
||||
case uri
|
||||
case name
|
||||
|
|
@ -152,23 +188,27 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
switch type {
|
||||
case "text":
|
||||
let text = try container.decode(String.self, forKey: .text)
|
||||
self = .text(text)
|
||||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .text(text: text, annotations: annotations, _meta: _meta)
|
||||
case "image":
|
||||
let data = try container.decode(String.self, forKey: .data)
|
||||
let mimeType = try container.decode(String.self, forKey: .mimeType)
|
||||
let _meta = try container.decodeIfPresent(
|
||||
Metadata.self, forKey: ._meta)
|
||||
self = .image(data: data, mimeType: mimeType, metadata: _meta)
|
||||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .image(data: data, mimeType: mimeType, annotations: annotations, _meta: _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)
|
||||
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
|
||||
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
|
||||
self = .audio(data: data, mimeType: mimeType, annotations: annotations, _meta: _meta)
|
||||
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":
|
||||
case "resource_link":
|
||||
let uri = try container.decode(String.self, forKey: .uri)
|
||||
let name = try container.decode(String.self, forKey: .name)
|
||||
let title = try container.decodeIfPresent(String.self, forKey: .title)
|
||||
|
|
@ -189,18 +229,23 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .text(let text):
|
||||
case .text(let text, let annotations, let _meta):
|
||||
try container.encode("text", forKey: .type)
|
||||
try container.encode(text, forKey: .text)
|
||||
case .image(let data, let mimeType, let metadata):
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
case .image(let data, let mimeType, let annotations, let _meta):
|
||||
try container.encode("image", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(metadata, forKey: ._meta)
|
||||
case .audio(let data, let mimeType):
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
case .audio(let data, let mimeType, let annotations, let _meta):
|
||||
try container.encode("audio", forKey: .type)
|
||||
try container.encode(data, forKey: .data)
|
||||
try container.encode(mimeType, forKey: .mimeType)
|
||||
try container.encodeIfPresent(annotations, forKey: .annotations)
|
||||
try container.encodeIfPresent(_meta, forKey: ._meta)
|
||||
case .resource(let resourceContent, let annotations, let _meta):
|
||||
try container.encode("resource", forKey: .type)
|
||||
try container.encode(resourceContent, forKey: .resource)
|
||||
|
|
@ -208,7 +253,7 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
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("resource_link", forKey: .type)
|
||||
try container.encode(uri, forKey: .uri)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encodeIfPresent(title, forKey: .title)
|
||||
|
|
@ -261,7 +306,7 @@ public struct Tool: Hashable, Codable, Sendable {
|
|||
// MARK: -
|
||||
|
||||
/// To discover available tools, clients send a `tools/list` request.
|
||||
/// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-06-18/server/tools/#listing-tools
|
||||
/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/tools/#listing-tools
|
||||
public enum ListTools: Method {
|
||||
public static let name = "tools/list"
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue