From 9bd6e6679642c07ef2614f73624b787edd9dceee Mon Sep 17 00:00:00 2001 From: Yehor Sobko Date: Fri, 16 Jan 2026 18:31:08 +0100 Subject: [PATCH 1/2] feat(Tool): intermediate updates to the Tool before the general changes for newest protocol version --- Package.resolved | 4 +- Sources/MCP/Base/Annotations.swift | 40 +++ Sources/MCP/Base/Error.swift | 310 ++++++++++++++--- Sources/MCP/Base/Icon.swift | 51 +++ Sources/MCP/Base/Messages.swift | 12 +- Sources/MCP/Base/Progress.swift | 324 ++++++++++++++++++ .../MCP/Base/Transports/Types/AuthInfo.swift | 71 ++++ .../Base/Transports/Types/RequestInfo.swift | 43 +++ Sources/MCP/Client/Client.swift | 295 +++++++++++++++- Sources/MCP/Server/Resources.swift | 100 ++++++ .../MCP/Server/Server+RequestHandling.swift | 46 +++ Sources/MCP/Server/Server.swift | 307 +++++++++++++++++ Sources/MCP/Server/Tools.swift | 255 +++++++++++--- 13 files changed, 1753 insertions(+), 105 deletions(-) create mode 100644 Sources/MCP/Base/Annotations.swift create mode 100644 Sources/MCP/Base/Icon.swift create mode 100644 Sources/MCP/Base/Progress.swift create mode 100644 Sources/MCP/Base/Transports/Types/AuthInfo.swift create mode 100644 Sources/MCP/Base/Transports/Types/RequestInfo.swift create mode 100644 Sources/MCP/Server/Server+RequestHandling.swift diff --git a/Package.resolved b/Package.resolved index 5e9023c5..fb776dd5 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,10 +1,10 @@ { - "originHash" : "08de61941b7919a65e36c0e34f8c1c41995469b86a39122158b75b4a68c4527d", + "originHash" : "371f3dfcfa1201fc8d50e924ad31f9ebc4f90242924df1275958ac79df15dc12", "pins" : [ { "identity" : "eventsource", "kind" : "remoteSourceControl", - "location" : "https://github.com/loopwork-ai/eventsource.git", + "location" : "https://github.com/mattt/eventsource.git", "state" : { "revision" : "e83f076811f32757305b8bf69ac92d05626ffdd7", "version" : "1.1.0" diff --git a/Sources/MCP/Base/Annotations.swift b/Sources/MCP/Base/Annotations.swift new file mode 100644 index 00000000..0b6b62db --- /dev/null +++ b/Sources/MCP/Base/Annotations.swift @@ -0,0 +1,40 @@ +import Foundation + +/// The sender or recipient of messages and data in a conversation. +public enum Role: String, Hashable, Codable, Sendable { + /// A user message + case user + /// An assistant message + case assistant +} + +/// Optional annotations for content, used to inform how objects are used or displayed. +/// +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/schema#annotations +public struct Annotations: Hashable, Codable, Sendable { + // TODO: Deprecate in a future version + /// Backwards compatibility alias for top-level `Role`. + public typealias Role = MCP.Role + + /// Describes who the intended audience of this object or data is. + /// It can include multiple entries to indicate content useful for multiple audiences. + public var audience: [Role]? + + /// Describes how important this data is for operating the server. + /// A value of 1 means "most important" (effectively required), + /// while 0 means "least important" (entirely optional). + public var priority: Double? + + /// The moment the resource was last modified, as an ISO 8601 formatted string. + public var lastModified: String? + + public init( + audience: [Role]? = nil, + priority: Double? = nil, + lastModified: String? = nil + ) { + self.audience = audience + self.priority = priority + self.lastModified = lastModified + } +} diff --git a/Sources/MCP/Base/Error.swift b/Sources/MCP/Base/Error.swift index 0c461a46..16c1c919 100644 --- a/Sources/MCP/Base/Error.swift +++ b/Sources/MCP/Base/Error.swift @@ -6,6 +6,67 @@ import Foundation @preconcurrency import SystemPackage #endif +// MARK: - Error Codes + +/// JSON-RPC and MCP error codes. +/// +/// Error codes are organized by source: +/// - Standard JSON-RPC 2.0 error codes (-32700 to -32600) +/// - MCP specification error codes (-32002, -32042) +/// - SDK-specific error codes (-32000, -32001, -32003) +public enum ErrorCode { + // MARK: Standard JSON-RPC 2.0 Errors + + /// Parse error: Invalid JSON was received by the server. + public static let parseError: Int = -32700 + + /// Invalid request: The JSON sent is not a valid Request object. + public static let invalidRequest: Int = -32600 + + /// Method not found: The method does not exist or is not available. + public static let methodNotFound: Int = -32601 + + /// Invalid params: Invalid method parameter(s). + public static let invalidParams: Int = -32602 + + /// Internal error: Internal JSON-RPC error. + public static let internalError: Int = -32603 + + // MARK: MCP Specification Errors + + /// Resource not found: The requested resource does not exist. + /// + /// Defined in MCP specification (resources.mdx). + public static let resourceNotFound: Int = -32002 + + /// URL elicitation required: The request requires URL-mode elicitation(s) to be completed. + /// + /// Defined in MCP specification (schema). + public static let urlElicitationRequired: Int = -32042 + + // MARK: SDK-Specific Errors + + /// Connection closed: The connection to the server was closed. + /// + /// Not defined in MCP spec. SDK-specific, matches TypeScript SDK. + public static let connectionClosed: Int = -32000 + + /// Request timeout: The server did not respond within the timeout period. + /// + /// Not defined in MCP spec. SDK-specific, matches TypeScript SDK. + public static let requestTimeout: Int = -32001 + + /// Transport error: An error occurred in the transport layer. + /// + /// Not defined in MCP spec. SDK-specific for Swift. + public static let transportError: Int = -32003 + + /// Request cancelled: The request was cancelled before completion. + /// + /// Not defined in MCP spec. SDK-specific for Swift. + public static let requestCancelled: Int = -32004 +} + /// A model context protocol error. public enum MCPError: Swift.Error, Sendable { // Standard JSON-RPC 2.0 errors (-32700 to -32603) @@ -17,7 +78,9 @@ public enum MCPError: Swift.Error, Sendable { // Server errors (-32000 to -32099) case serverError(code: Int, message: String) - + // Request timeout + /// Request timed out waiting for a response. + case requestTimeout(timeout: Duration, message: String?) // Transport specific errors case connectionClosed case transportError(Swift.Error) @@ -25,17 +88,67 @@ public enum MCPError: Swift.Error, Sendable { /// The JSON-RPC 2.0 error code public var code: Int { switch self { - case .parseError: return -32700 - case .invalidRequest: return -32600 - case .methodNotFound: return -32601 - case .invalidParams: return -32602 - case .internalError: return -32603 + case .parseError: return ErrorCode.parseError + case .invalidRequest: return ErrorCode.invalidRequest + case .methodNotFound: return ErrorCode.methodNotFound + case .invalidParams: return ErrorCode.invalidParams + case .internalError: return ErrorCode.internalError case .serverError(let code, _): return code - case .connectionClosed: return -32000 - case .transportError: return -32001 + case .connectionClosed: return ErrorCode.connectionClosed + case .transportError: return ErrorCode.transportError + case .requestTimeout: return ErrorCode.requestTimeout } } - + + /// The raw error message for wire format serialization. + /// + /// This returns the message suitable for JSON-RPC 2.0 error format, without + /// any additional prefixes or formatting that `errorDescription` might add. + /// Use this for serialization; use `errorDescription` for human-readable display. + public var message: String { + switch self { + case .parseError(let detail): + return detail ?? "Invalid JSON" + case .invalidRequest(let detail): + return detail ?? "Invalid Request" + case .methodNotFound(let detail): + return detail ?? "Method not found" + case .invalidParams(let detail): + return detail ?? "Invalid params" + case .internalError(let detail): + return detail ?? "Internal error" + case .serverError(_, let message): + return message + case .connectionClosed: + return "Connection closed" + case .transportError(let error): + return error.localizedDescription + case .requestTimeout(let timeout, let message): + return message ?? "Request timed out after \(timeout)" + } + } + + /// The error data payload for wire format serialization. + /// + /// This returns the additional data to include in the JSON-RPC 2.0 error, + /// following MCP specification requirements for specific error types. + public var data: Value? { + switch self { + case .parseError, .invalidRequest, .methodNotFound, .invalidParams, .internalError: + // Standard JSON-RPC errors don't require data + return nil + case .serverError: + return nil + case .connectionClosed: + return nil + case .transportError(let error): + return .object(["error": .string(error.localizedDescription)]) + case .requestTimeout(let timeout, _): + let timeoutMs = Int(timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000) + return .object(["timeout": .int(timeoutMs)]) + } + } + /// Check if an error represents a "resource temporarily unavailable" condition public static func isResourceTemporarilyUnavailable(_ error: Swift.Error) -> Bool { #if canImport(System) @@ -72,6 +185,12 @@ extension MCPError: LocalizedError { return "Connection closed" case .transportError(let error): return "Transport error: \(error.localizedDescription)" + case .requestTimeout(let timeout, let message): + if let message { + return "Request timeout: \(message)" + } else { + return "Request timed out after \(timeout)" + } } } @@ -93,6 +212,8 @@ extension MCPError: LocalizedError { return "The connection to the server was closed" case .transportError(let error): return (error as? LocalizedError)?.failureReason ?? error.localizedDescription + case .requestTimeout: + return "The server did not respond within the timeout period" } } @@ -139,28 +260,10 @@ extension MCPError: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(code, forKey: .code) - try container.encode(errorDescription ?? "Unknown error", forKey: .message) - - // Encode additional data if available - switch self { - case .parseError(let detail), - .invalidRequest(let detail), - .methodNotFound(let detail), - .invalidParams(let detail), - .internalError(let detail): - if let detail = detail { - try container.encode(["detail": detail], forKey: .data) - } - case .serverError(_, _): - // No additional data for server errors - break - case .connectionClosed: - break - case .transportError(let error): - try container.encode( - ["error": error.localizedDescription], - forKey: .data - ) + try container.encode(message, forKey: .message) + // Encode data if available + if let data = self.data { + try container.encode(data, forKey: .data) } } @@ -168,35 +271,45 @@ extension MCPError: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) let code = try container.decode(Int.self, forKey: .code) let message = try container.decode(String.self, forKey: .message) - let data = try container.decodeIfPresent([String: Value].self, forKey: .data) - - // Helper to extract detail from data, falling back to message if needed - let unwrapDetail: (String?) -> String? = { fallback in - guard let detailValue = data?["detail"] else { return fallback } - if case .string(let str) = detailValue { return str } - return fallback + // Try to decode data as a generic Value first + let dataValue = try container.decodeIfPresent(Value.self, forKey: .data) + + // Helper to check if message is the default for a given error type. + // If it's the default, we use nil as the detail; otherwise we use the custom message. + func customDetailOrNil(ifNotDefault defaultMessage: String) -> String? { + message == defaultMessage ? nil : message } - + switch code { - case -32700: - self = .parseError(unwrapDetail(message)) - case -32600: - self = .invalidRequest(unwrapDetail(message)) - case -32601: - self = .methodNotFound(unwrapDetail(message)) - case -32602: - self = .invalidParams(unwrapDetail(message)) - case -32603: - self = .internalError(unwrapDetail(nil)) - case -32000: + case ErrorCode.parseError: + self = .parseError(customDetailOrNil(ifNotDefault: "Invalid JSON")) + case ErrorCode.invalidRequest: + self = .invalidRequest(customDetailOrNil(ifNotDefault: "Invalid Request")) + case ErrorCode.methodNotFound: + self = .methodNotFound(customDetailOrNil(ifNotDefault: "Method not found")) + case ErrorCode.invalidParams: + self = .invalidParams(customDetailOrNil(ifNotDefault: "Invalid params")) + case ErrorCode.internalError: + self = .internalError(customDetailOrNil(ifNotDefault: "Internal error")) + + case ErrorCode.connectionClosed: self = .connectionClosed - case -32001: + case ErrorCode.requestTimeout: + // Extract timeout from data if present + var timeoutMs = 60000 // Default 60 seconds + if case .object(let dict) = dataValue, + let timeoutValue = dict["timeout"], + case .int(let t) = timeoutValue { + timeoutMs = t + } + self = .requestTimeout(timeout: .milliseconds(timeoutMs), message: message) + case ErrorCode.transportError: // Extract underlying error string if present - let underlyingErrorString = - data?["error"].flatMap { val -> String? in - if case .string(let str) = val { return str } - return nil - } ?? message + var underlyingErrorString = message + if case .object(let dict) = dataValue, + case .string(let str) = dict["error"] { + underlyingErrorString = str + } self = .transportError( NSError( domain: "org.jsonrpc.error", @@ -208,13 +321,93 @@ extension MCPError: Codable { self = .serverError(code: code, message: message) } } + + /// Reconstructs an MCPError from error code, message, and optional data. + /// + /// This is useful for clients receiving error responses and wanting to + /// work with typed error values. + /// + /// - Parameters: + /// - code: The JSON-RPC error code + /// - message: The error message + /// - data: Optional additional error data + /// - Returns: The appropriate MCPError type + public static func fromError(code: Int, message: String, data: Value? = nil) -> MCPError { + // Helper to check if message is the default for a given error type. + // If it's the default, we use nil as the detail; otherwise we use the custom message. + func customDetailOrNil(ifNotDefault defaultMessage: String) -> String? { + message == defaultMessage ? nil : message + } + + switch code { + case ErrorCode.parseError: + return .parseError(customDetailOrNil(ifNotDefault: "Invalid JSON")) + case ErrorCode.invalidRequest: + return .invalidRequest(customDetailOrNil(ifNotDefault: "Invalid Request")) + case ErrorCode.methodNotFound: + return .methodNotFound(customDetailOrNil(ifNotDefault: "Method not found")) + case ErrorCode.invalidParams: + return .invalidParams(customDetailOrNil(ifNotDefault: "Invalid params")) + case ErrorCode.internalError: + return .internalError(customDetailOrNil(ifNotDefault: "Internal error")) + case ErrorCode.connectionClosed: + return .connectionClosed + case ErrorCode.requestTimeout: + // Extract timeout from data if present + var timeoutMs = 60000 // Default 60 seconds + if case .object(let dict) = data, + let timeoutValue = dict["timeout"], + case .int(let t) = timeoutValue { + timeoutMs = t + } + return .requestTimeout(timeout: .milliseconds(timeoutMs), message: message) + case ErrorCode.transportError: + // Extract underlying error string if present + var underlyingErrorString = message + if case .object(let dict) = data, + case .string(let str) = dict["error"] { + underlyingErrorString = str + } + return .transportError( + NSError( + domain: "org.jsonrpc.error", + code: code, + userInfo: [NSLocalizedDescriptionKey: underlyingErrorString] + ) + ) + + default: + return .serverError(code: code, message: message) + } + } } // MARK: Equatable extension MCPError: Equatable { public static func == (lhs: MCPError, rhs: MCPError) -> Bool { - lhs.code == rhs.code + switch (lhs, rhs) { + case (.parseError(let l), .parseError(let r)): + return l == r + case (.invalidRequest(let l), .invalidRequest(let r)): + return l == r + case (.methodNotFound(let l), .methodNotFound(let r)): + return l == r + case (.invalidParams(let l), .invalidParams(let r)): + return l == r + case (.internalError(let l), .internalError(let r)): + return l == r + case (.serverError(let lCode, let lMsg), .serverError(let rCode, let rMsg)): + return lCode == rCode && lMsg == rMsg + case (.connectionClosed, .connectionClosed): + return true + case (.transportError(let l), .transportError(let r)): + return l.localizedDescription == r.localizedDescription + case (.requestTimeout(let lTimeout, let lMsg), .requestTimeout(let rTimeout, let rMsg)): + return lTimeout == rTimeout && lMsg == rMsg + default: + return false + } } } @@ -240,6 +433,9 @@ extension MCPError: Hashable { break case .transportError(let error): hasher.combine(error.localizedDescription) + case .requestTimeout(let timeout, let message): + hasher.combine(timeout) + hasher.combine(message) } } } diff --git a/Sources/MCP/Base/Icon.swift b/Sources/MCP/Base/Icon.swift new file mode 100644 index 00000000..c95dad89 --- /dev/null +++ b/Sources/MCP/Base/Icon.swift @@ -0,0 +1,51 @@ +import Foundation + +/// Icon metadata for representing visual icons for tools, resources, prompts, and implementations. +/// +/// Icons can be provided as HTTP/HTTPS URLs or data URIs (base64-encoded images). +/// +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/schema#icon +public struct Icon: Hashable, Codable, Sendable { + /// URL or data URI for the icon. + /// + /// Can be an HTTP/HTTPS URL or a data URI (e.g., `data:image/png;base64,...`). + public let src: String + + /// Optional MIME type for the icon. + /// + /// Useful when the MIME type cannot be inferred from the `src` URL. + public let mimeType: String? + + /// Optional array of strings that specify sizes at which the icon can be used. + /// + /// Each string should be in WxH format (e.g., `"48x48"`, `"96x96"`) or `"any"` for + /// scalable formats like SVG. + /// + /// If not provided, the client should assume that the icon can be used at any size. + public let sizes: [String]? + + /// Optional specifier for the theme this icon is designed for. + /// + /// If not provided, the client should assume the icon can be used with any theme. + public let theme: Theme? + + /// The theme an icon is designed for. + public enum Theme: String, Hashable, Codable, Sendable { + /// Icon designed for use with a light background. + case light + /// Icon designed for use with a dark background. + case dark + } + + public init( + src: String, + mimeType: String? = nil, + sizes: [String]? = nil, + theme: Theme? = nil + ) { + self.src = src + self.mimeType = mimeType + self.sizes = sizes + self.theme = theme + } +} diff --git a/Sources/MCP/Base/Messages.swift b/Sources/MCP/Base/Messages.swift index b9058f7e..dda1b6d2 100644 --- a/Sources/MCP/Base/Messages.swift +++ b/Sources/MCP/Base/Messages.swift @@ -291,8 +291,18 @@ extension AnyNotification { } } + +/// Protocol for type-erased notification messages. +/// +/// This protocol allows sending notification messages with parameters through +/// a type-erased interface. `Message` conforms to this protocol. +public protocol NotificationMessageProtocol: Sendable, Encodable { + /// The notification method name. + var method: String { get } +} + /// A message that can be used to send notifications. -public struct Message: Hashable, Codable, Sendable { +public struct Message: NotificationMessageProtocol, Hashable, Codable, Sendable { /// The method name. public let method: String /// The notification parameters. diff --git a/Sources/MCP/Base/Progress.swift b/Sources/MCP/Base/Progress.swift new file mode 100644 index 00000000..e21e4204 --- /dev/null +++ b/Sources/MCP/Base/Progress.swift @@ -0,0 +1,324 @@ +/// Progress tracking for long-running operations. +/// +/// Clients can include a `progressToken` in request metadata (`_meta.progressToken`) +/// to receive progress notifications during operation execution. +/// +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress + +/// Metadata that can be attached to any request via the `_meta` field. +/// +/// This is used primarily for progress tracking, but can also carry +/// arbitrary additional metadata. +public struct RequestMeta: Hashable, Codable, Sendable { + /// If specified, the caller is requesting out-of-band progress notifications + /// for this request. The value is an opaque token that will be attached to + /// any subsequent progress notifications. + public var progressToken: ProgressToken? + + /// Additional metadata fields. + public var additionalFields: [String: Value]? + + public init( + progressToken: ProgressToken? = nil, + additionalFields: [String: Value]? = nil + ) { + self.progressToken = progressToken + self.additionalFields = additionalFields + } + + // MARK: - Convenience Accessors + + /// The related task ID, if present. + /// + /// Extracts the task ID from `_meta["io.modelcontextprotocol/related-task"].taskId`. + /// This matches the TypeScript SDK's `_meta[RELATED_TASK_META_KEY]?.taskId`. + /// + /// ## Example + /// + /// ```swift + /// if let taskId = context._meta?.relatedTaskId { + /// print("Request is part of task: \(taskId)") + /// } + /// ``` + /// + /// - Note: For the full `RelatedTaskMetadata` struct, use the experimental tasks API. + public var relatedTaskId: String? { + guard let metaValue = additionalFields?["io.modelcontextprotocol/related-task"], + case .object(let dict) = metaValue, + let taskIdValue = dict["taskId"], + let taskId = taskIdValue.stringValue else { + return nil + } + return taskId + } + + private enum CodingKeys: String, CodingKey { + case progressToken + } + + private struct DynamicCodingKey: CodingKey { + var stringValue: String + var intValue: Int? { nil } + + init?(stringValue: String) { + self.stringValue = stringValue + } + + init?(intValue: Int) { + return nil + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + progressToken = try container.decodeIfPresent(ProgressToken.self, forKey: .progressToken) + + // Decode additional fields + let dynamicContainer = try decoder.container(keyedBy: DynamicCodingKey.self) + var extra: [String: Value] = [:] + for key in dynamicContainer.allKeys { + if key.stringValue == CodingKeys.progressToken.stringValue { + continue + } + if let value = try? dynamicContainer.decode(Value.self, forKey: key) { + extra[key.stringValue] = value + } + } + additionalFields = extra.isEmpty ? nil : extra + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encodeIfPresent(progressToken, forKey: .progressToken) + + // Encode additional fields + if let additional = additionalFields { + var dynamicContainer = encoder.container(keyedBy: DynamicCodingKey.self) + for (key, value) in additional { + if let codingKey = DynamicCodingKey(stringValue: key) { + try dynamicContainer.encode(value, forKey: codingKey) + } + } + } + } +} + +/// A token used to associate progress notifications with a specific request. +/// +/// Progress tokens can be either strings or integers. +public enum ProgressToken: Hashable, Sendable { + case string(String) + case integer(Int) +} + +extension ProgressToken: Codable { + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let intValue = try? container.decode(Int.self) { + self = .integer(intValue) + } else if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else { + throw DecodingError.typeMismatch( + ProgressToken.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Expected string or integer for ProgressToken" + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .integer(let value): + try container.encode(value) + } + } +} + +extension ProgressToken: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self = .string(value) + } +} + +extension ProgressToken: ExpressibleByIntegerLiteral { + public init(integerLiteral value: Int) { + self = .integer(value) + } +} + +/// Notification sent to report progress on a long-running operation. +/// +/// Servers send progress notifications to inform clients about the status +/// of operations that may take significant time to complete. +/// +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress +public struct ProgressNotification: Notification { + public static let name: String = "notifications/progress" + + public struct Parameters: Hashable, Codable, Sendable { + /// The progress token from the original request's `_meta.progressToken`. + public let progressToken: ProgressToken + + /// The current progress value. Should increase monotonically. + public let progress: Double + + /// The total progress value, if known. + public let total: Double? + + /// An optional human-readable message describing the current progress. + public let message: String? + + /// Reserved for additional metadata. + public var _meta: [String: Value]? + + public init( + progressToken: ProgressToken, + progress: Double, + total: Double? = nil, + message: String? = nil, + _meta: [String: Value]? = nil + ) { + self.progressToken = progressToken + self.progress = progress + self.total = total + self.message = message + self._meta = _meta + } + } +} + +// MARK: - Progress Callback + +/// Progress information received during a long-running operation. +/// +/// This struct is passed to progress callbacks when using `send(_:onProgress:)`. +public struct Progress: Sendable, Hashable { + /// The current progress value. Increases monotonically. + public let value: Double + + /// The total progress value, if known. + public let total: Double? + + /// An optional human-readable message describing current progress. + public let message: String? + + public init(value: Double, total: Double? = nil, message: String? = nil) { + self.value = value + self.total = total + self.message = message + } +} + +/// A callback invoked when a progress notification is received. +/// +/// This is used by the client to receive progress updates for specific requests +/// when using `send(_:onProgress:)`. +public typealias ProgressCallback = @Sendable (Progress) async -> Void + +// MARK: - Progress Tracker (Server-Side) + +/// An actor for tracking and sending cumulative progress during a request. +/// +/// This follows the Python SDK's `ProgressContext` pattern, providing a convenient +/// way to track cumulative progress and send notifications without manually +/// tracking the current value. +/// +/// ## Example +/// +/// ```swift +/// server.withRequestHandler(CallTool.self) { request, context in +/// guard let token = request._meta?.progressToken else { +/// return CallTool.Result(content: [.text("Done")]) +/// } +/// +/// let tracker = ProgressTracker(token: token, total: 100, context: context) +/// +/// try await tracker.advance(by: 25, message: "Loading...") +/// try await tracker.advance(by: 50, message: "Processing...") +/// try await tracker.advance(by: 25, message: "Completing...") +/// +/// return CallTool.Result(content: [.text("Done")]) +/// } +/// ``` +public actor ProgressTracker { + /// The progress token from the request. + public let token: ProgressToken + + /// The total progress value, if known. + public let total: Double? + + /// The request handler context for sending notifications. + private let context: Server.RequestHandlerContext + + /// The current cumulative progress value. + public private(set) var current: Double = 0 + + /// Creates a new progress tracker. + /// + /// - Parameters: + /// - token: The progress token from the request's `_meta.progressToken` + /// - total: The total progress value, if known + /// - context: The request handler context for sending notifications + public init( + token: ProgressToken, + total: Double? = nil, + context: Server.RequestHandlerContext + ) { + self.token = token + self.total = total + self.context = context + } + + /// Advance progress by the given amount and send a notification. + /// + /// - Parameters: + /// - amount: The amount to add to the current progress + /// - message: An optional human-readable message describing current progress + public func advance(by amount: Double, message: String? = nil) async throws { + current += amount + try await context.sendProgress( + token: token, + progress: current, + total: total, + message: message + ) + } + + /// Set progress to a specific value and send a notification. + /// + /// Use this when you want to set progress to an absolute value rather than + /// incrementing. The progress value should still increase monotonically. + /// + /// - Parameters: + /// - value: The new progress value + /// - message: An optional human-readable message describing current progress + public func set(to value: Double, message: String? = nil) async throws { + current = value + try await context.sendProgress( + token: token, + progress: current, + total: total, + message: message + ) + } + + /// Send a progress notification without changing the current value. + /// + /// Use this to update the message without changing the progress value. + /// + /// - Parameter message: A human-readable message describing current progress + public func update(message: String) async throws { + try await context.sendProgress( + token: token, + progress: current, + total: total, + message: message + ) + } +} diff --git a/Sources/MCP/Base/Transports/Types/AuthInfo.swift b/Sources/MCP/Base/Transports/Types/AuthInfo.swift new file mode 100644 index 00000000..55b0dcf2 --- /dev/null +++ b/Sources/MCP/Base/Transports/Types/AuthInfo.swift @@ -0,0 +1,71 @@ +import Foundation + +/// Information about a validated access token. +/// +/// This struct contains authentication context that can be provided to request handlers +/// when using HTTP transports with OAuth or other token-based authentication. +/// +/// Matches the TypeScript SDK's `AuthInfo` interface. +/// +/// ## Example +/// +/// ```swift +/// server.withRequestHandler(CallTool.self) { params, context in +/// if let authInfo = context.authInfo { +/// print("Authenticated as: \(authInfo.clientId)") +/// print("Scopes: \(authInfo.scopes)") +/// } +/// return CallTool.Result(content: [.text("Done")]) +/// } +/// ``` +public struct AuthInfo: Hashable, Codable, Sendable { + /// The access token string. + public let token: String + + /// The client ID associated with this token. + public let clientId: String + + /// Scopes associated with this token. + public let scopes: [String] + + /// When the token expires (in seconds since epoch). + /// + /// If `nil`, the token does not expire or expiration is unknown. + public let expiresAt: Int? + + /// The RFC 8707 resource server identifier for which this token is valid. + /// + /// If set, this should match the MCP server's resource identifier (minus hash fragment). + public let resource: String? + + /// Additional data associated with the token. + /// + /// Use this for any additional data that needs to be attached to the auth info. + public let extra: [String: Value]? + + public init( + token: String, + clientId: String, + scopes: [String], + expiresAt: Int? = nil, + resource: String? = nil, + extra: [String: Value]? = nil + ) { + self.token = token + self.clientId = clientId + self.scopes = scopes + self.expiresAt = expiresAt + self.resource = resource + self.extra = extra + } +} + +extension AuthInfo: CustomStringConvertible { + /// Redacts the token to prevent accidental exposure in logs. + /// + /// The token is still accessible via the `token` property for legitimate use, + /// but this prevents it from appearing in string interpolation or print statements. + public var description: String { + "AuthInfo(clientId: \(clientId), scopes: \(scopes), token: [REDACTED])" + } +} diff --git a/Sources/MCP/Base/Transports/Types/RequestInfo.swift b/Sources/MCP/Base/Transports/Types/RequestInfo.swift new file mode 100644 index 00000000..f790c235 --- /dev/null +++ b/Sources/MCP/Base/Transports/Types/RequestInfo.swift @@ -0,0 +1,43 @@ +/// Information about the incoming HTTP request. +/// +/// This is the Swift equivalent of TypeScript's `RequestInfo` interface, which +/// provides access to HTTP request headers for request handlers. +/// +/// ## Example +/// +/// ```swift +/// server.withRequestHandler(CallTool.self) { params, context in +/// if let requestInfo = context.requestInfo { +/// // Access custom headers +/// if let customHeader = requestInfo.headers["X-Custom-Header"] { +/// print("Custom header: \(customHeader)") +/// } +/// } +/// return CallTool.Result(content: [.text("Done")]) +/// } +/// ``` +public struct RequestInfo: Hashable, Sendable { + /// The HTTP headers from the request. + /// + /// Header names are preserved as provided by the HTTP framework. + /// Use case-insensitive comparison when looking up headers. + public let headers: [String: String] + + public init(headers: [String: String]) { + self.headers = headers + } + + /// Get a header value (case-insensitive lookup). + /// + /// - Parameter name: The header name to look up + /// - Returns: The header value, or nil if not found + public func header(_ name: String) -> String? { + let lowercased = name.lowercased() + for (key, value) in headers { + if key.lowercased() == lowercased { + return value + } + } + return nil + } +} diff --git a/Sources/MCP/Client/Client.swift b/Sources/MCP/Client/Client.swift index 696ffd14..8092af65 100644 --- a/Sources/MCP/Client/Client.swift +++ b/Sources/MCP/Client/Client.swift @@ -77,6 +77,173 @@ public actor Client { self.roots = roots } } + + /// Context provided to client request handlers. + /// + /// This context is passed to handlers for server→client requests (e.g., sampling, + /// elicitation, roots) and provides: + /// - Cancellation checking via `isCancelled` and `checkCancellation()` + /// - Notification sending to the server + /// - Progress reporting convenience methods + /// + /// ## Example + /// + /// ```swift + /// client.withRequestHandler(CreateSamplingMessage.self) { params, context in + /// // Check for cancellation periodically + /// try context.checkCancellation() + /// + /// // Report progress back to server + /// try await context.sendProgressNotification( + /// token: progressToken, + /// progress: 50.0, + /// total: 100.0, + /// message: "Processing..." + /// ) + /// + /// return result + /// } + /// ``` + public struct RequestHandlerContext: Sendable { + /// Send a notification to the server. + /// + /// Use this to send notifications from within a request handler. + let sendNotification: @Sendable (any NotificationMessageProtocol) async throws -> Void + + /// The JSON-RPC ID of the request being handled. + /// + /// This can be useful for tracking, logging, or correlating messages. + /// It matches the TypeScript SDK's `extra.requestId`. + public let requestId: ID + + /// The request metadata from the `_meta` field, if present. + /// + /// Contains metadata like the progress token for progress notifications. + /// This matches the TypeScript SDK's `extra._meta` and Python's `ctx.meta`. + /// + /// ## Example + /// + /// ```swift + /// client.withRequestHandler(CreateSamplingMessage.self) { params, context in + /// if let progressToken = context._meta?.progressToken { + /// try await context.sendProgressNotification( + /// token: progressToken, + /// progress: 50, + /// total: 100 + /// ) + /// } + /// return result + /// } + /// ``` + public let _meta: RequestMeta? + + /// The task ID for task-augmented requests, if present. + /// + /// This is a convenience property that extracts the task ID from the + /// `_meta["io.modelcontextprotocol/related-task"]` field. When a server + /// sends a task-augmented elicitation or sampling request, this property + /// will contain the associated task ID. + /// + /// This matches the TypeScript SDK's `extra.taskId` and aligns with + /// `Server.RequestHandlerContext.taskId`. + /// + /// ## Example + /// + /// ```swift + /// client.withElicitationHandler { params, context in + /// if let taskId = context.taskId { + /// print("Handling elicitation for task: \(taskId)") + /// } + /// return ElicitResult(action: .accept, content: [:]) + /// } + /// ``` + public var taskId: String? { + _meta?.relatedTaskId + } + + // MARK: - Convenience Methods + + /// Send a progress notification to the server. + /// + /// Use this to report progress on long-running operations initiated by + /// server→client requests. + /// + /// - Parameters: + /// - token: The progress token from the request's `_meta.progressToken` + /// - progress: The current progress value (should increase monotonically) + /// - total: The total progress value, if known + /// - message: An optional human-readable message describing current progress + public func sendProgressNotification( + token: ProgressToken, + progress: Double, + total: Double? = nil, + message: String? = nil + ) async throws { + try await sendNotification(ProgressNotification.message(.init( + progressToken: token, + progress: progress, + total: total, + message: message + ))) + } + + // MARK: - Cancellation Checking + + /// Whether the request has been cancelled. + /// + /// Check this property periodically during long-running operations + /// to respond to cancellation requests from the server. + /// + /// This returns `true` when: + /// - The server sends a `CancelledNotification` for this request + /// - The client is disconnecting + /// + /// When cancelled, the handler should clean up resources and return + /// or throw an error. Per MCP spec, responses are not sent for cancelled requests. + /// + /// ## Example + /// + /// ```swift + /// client.withRequestHandler(CreateSamplingMessage.self) { params, context in + /// for chunk in largeInput { + /// // Check cancellation periodically + /// guard !context.isCancelled else { + /// throw CancellationError() + /// } + /// try await process(chunk) + /// } + /// return result + /// } + /// ``` + public var isCancelled: Bool { + Task.isCancelled + } + + /// Check if the request has been cancelled and throw if so. + /// + /// Call this method periodically during long-running operations. + /// If the request has been cancelled, this throws `CancellationError`. + /// + /// This is equivalent to checking `isCancelled` and throwing manually, + /// but provides a more idiomatic Swift concurrency pattern. + /// + /// ## Example + /// + /// ```swift + /// client.withRequestHandler(CreateSamplingMessage.self) { params, context in + /// for chunk in largeInput { + /// try context.checkCancellation() // Throws if cancelled + /// try await process(chunk) + /// } + /// return result + /// } + /// ``` + /// + /// - Throws: `CancellationError` if the request has been cancelled. + public func checkCancellation() throws { + try Task.checkCancellation() + } + } /// The connection to the server private var connection: (any Transport)? @@ -150,13 +317,137 @@ public actor Client { _resume(.failure(error)) } } - + /// A dictionary of type-erased pending requests, keyed by request ID private var pendingRequests: [ID: AnyPendingRequest] = [:] + /// Progress callbacks for requests, keyed by progress token. + /// Used to invoke callbacks when progress notifications are received. + private var progressCallbacks: [ProgressToken: ProgressCallback] = [:] + /// Timeout controllers for requests with progress-aware timeouts. + /// Used to reset timeouts when progress notifications are received. + private var timeoutControllers: [ProgressToken: TimeoutController] = [:] + /// Mapping from request ID to progress token. + /// Used to detect task-augmented responses and keep progress handlers alive. + private var requestProgressTokens: [ID: ProgressToken] = [:] + /// Mapping from task ID to progress token. + /// Keeps progress handlers alive for task-augmented requests until the task completes. + /// Per MCP spec 2025-11-25: "For task-augmented requests, the progressToken provided + /// in the original request MUST continue to be used for progress notifications + /// throughout the task's lifetime, even after the CreateTaskResult has been returned." + private var taskProgressTokens: [String: ProgressToken] = [:] // Add reusable JSON encoder/decoder private let encoder = JSONEncoder() private let decoder = JSONDecoder() - + + /// Controls timeout behavior for a single request, supporting reset on progress. + /// + /// This actor manages the timeout state for requests that use `resetTimeoutOnProgress`. + /// When progress is received, calling `signalProgress()` resets the timeout clock. + actor TimeoutController { + /// The per-interval timeout duration. + let timeout: Duration + /// Whether to reset timeout when progress is received. + let resetOnProgress: Bool + /// Maximum total time to wait regardless of progress. + let maxTotalTimeout: Duration? + /// The start time of the request (for maxTotalTimeout tracking). + let startTime: ContinuousClock.Instant + /// The current deadline (updated when progress is received). + private var deadline: ContinuousClock.Instant + /// Whether the controller has been cancelled. + private var isCancelled = false + /// Continuation for signaling progress. + private var progressContinuation: AsyncStream.Continuation? + + init(timeout: Duration, resetOnProgress: Bool, maxTotalTimeout: Duration?) { + self.timeout = timeout + self.resetOnProgress = resetOnProgress + self.maxTotalTimeout = maxTotalTimeout + self.startTime = ContinuousClock.now + self.deadline = ContinuousClock.now.advanced(by: timeout) + } + + /// Signal that progress was received, resetting the timeout. + func signalProgress() { + guard resetOnProgress, !isCancelled else { return } + deadline = ContinuousClock.now.advanced(by: timeout) + progressContinuation?.yield() + } + + /// Cancel the timeout controller. + func cancel() { + isCancelled = true + progressContinuation?.finish() + } + + /// Wait until the timeout expires. + /// + /// If `resetOnProgress` is true, the timeout resets each time `signalProgress()` is called. + /// If `maxTotalTimeout` is set, the wait will end when that limit is exceeded. + /// + /// - Throws: `MCPError.requestTimeout` when the timeout expires. + func waitForTimeout() async throws { + let clock = ContinuousClock() + + // Create a stream for progress signals + let (progressStream, continuation) = AsyncStream.makeStream() + self.progressContinuation = continuation + + while !isCancelled { + // Check maxTotalTimeout + if let maxTotal = maxTotalTimeout { + let elapsed = clock.now - startTime + if elapsed >= maxTotal { + throw MCPError.requestTimeout( + timeout: maxTotal, + message: "Request exceeded maximum total timeout" + ) + } + } + + // Calculate time until deadline + let now = clock.now + let timeUntilDeadline = deadline - now + + if timeUntilDeadline <= .zero { + throw MCPError.requestTimeout( + timeout: timeout, + message: "Request timed out" + ) + } + + // Wait for either timeout or progress signal + do { + try await withThrowingTaskGroup(of: Void.self) { group in + // Timeout task + group.addTask { + try await Task.sleep(for: timeUntilDeadline) + } + + // Progress signal task (if reset is enabled) + if resetOnProgress { + group.addTask { + for await _ in progressStream { + // Progress received, exit to recalculate deadline + return + } + } + } + + // Wait for whichever completes first + _ = try await group.next() + group.cancelAll() + } + } catch is CancellationError { + return // Task was cancelled, exit gracefully + } + + // If we get here after a progress signal, loop to recalculate deadline + // If we get here after timeout, the next iteration will throw + } + } + } + public init( name: String, version: String, diff --git a/Sources/MCP/Server/Resources.swift b/Sources/MCP/Server/Resources.swift index 12f67335..0be14553 100644 --- a/Sources/MCP/Server/Resources.swift +++ b/Sources/MCP/Server/Resources.swift @@ -92,6 +92,106 @@ public struct Resource: Hashable, Codable, Sendable { } } +// MARK: - +/// A resource link returned in tool results, referencing a resource that can be read. +/// +/// Resource links differ from embedded resources in that they don't include +/// the actual content - they're references to resources that can be read later. +/// +/// Note: Resource links returned by tools are not guaranteed to appear +/// in the results of `resources/list` requests. +/// +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/schema#resourcelink +public struct ResourceLink: Hashable, Codable, Sendable { + /// The resource name (intended for programmatic or logical use) + public var name: String + /// A human-readable title for the resource, intended for UI display. + public var title: String? + /// The resource URI + public var uri: String + /// The resource description + public var description: String? + /// The resource MIME type + public var mimeType: String? + /// The size of the raw resource content, in bytes, if known. + public var size: Int? + /// Optional annotations for the client. + public var annotations: Annotations? + /// Optional icons representing this resource. + public var icons: [Icon]? + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + + public init( + name: String, + title: String? = nil, + uri: String, + description: String? = nil, + mimeType: String? = nil, + size: Int? = nil, + annotations: Annotations? = nil, + icons: [Icon]? = nil, + _meta: [String: Value]? = nil + ) { + self.name = name + self.title = title + self.uri = uri + self.description = description + self.mimeType = mimeType + self.size = size + self.annotations = annotations + self.icons = icons + self._meta = _meta + } + + private enum CodingKeys: String, CodingKey { + case type + case name + case title + case uri + case description + case mimeType + case size + case annotations + case icons + case _meta + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + // Verify type is "resource_link" + let type = try container.decodeIfPresent(String.self, forKey: .type) + if let type, type != "resource_link" { + throw DecodingError.dataCorruptedError( + forKey: .type, in: container, + debugDescription: "Expected type 'resource_link', got '\(type)'") + } + name = try container.decode(String.self, forKey: .name) + title = try container.decodeIfPresent(String.self, forKey: .title) + uri = try container.decode(String.self, forKey: .uri) + description = try container.decodeIfPresent(String.self, forKey: .description) + mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType) + size = try container.decodeIfPresent(Int.self, forKey: .size) + annotations = try container.decodeIfPresent(Annotations.self, forKey: .annotations) + icons = try container.decodeIfPresent([Icon].self, forKey: .icons) + _meta = try container.decodeIfPresent([String: Value].self, forKey: ._meta) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode("resource_link", forKey: .type) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(title, forKey: .title) + try container.encode(uri, forKey: .uri) + try container.encodeIfPresent(description, forKey: .description) + try container.encodeIfPresent(mimeType, forKey: .mimeType) + try container.encodeIfPresent(size, forKey: .size) + try container.encodeIfPresent(annotations, forKey: .annotations) + try container.encodeIfPresent(icons, forKey: .icons) + try container.encodeIfPresent(_meta, forKey: ._meta) + } +} + // MARK: - /// To discover available resources, clients send a `resources/list` request. diff --git a/Sources/MCP/Server/Server+RequestHandling.swift b/Sources/MCP/Server/Server+RequestHandling.swift new file mode 100644 index 00000000..81eb3cb4 --- /dev/null +++ b/Sources/MCP/Server/Server+RequestHandling.swift @@ -0,0 +1,46 @@ +import Foundation + +extension Server { + // MARK: - Request and Message Handling + + /// Internal context for routing responses to the correct transport. + /// + /// When handling requests, we capture the current connection at request time. + /// This ensures that when the handler completes (which may be async), the response + /// is sent to the correct client even if `self.connection` has changed in the meantime. + /// + /// This pattern is critical for HTTP transports where multiple clients can connect + /// and the server's `connection` reference gets reassigned. + struct RequestContext { + /// The transport connection captured at request time + let capturedConnection: (any Transport)? + /// The ID of the request being handled + let requestId: ID + /// The session ID from the transport, if available. + /// + /// For HTTP transports with multiple concurrent clients, this identifies + /// the specific session. Used for per-session features like log levels. + let sessionId: String? + /// The request metadata from `_meta` field, if present. + /// + /// Contains the progress token and any additional metadata. + let meta: RequestMeta? + /// Authentication information, if available. + /// + /// Set by HTTP transports when OAuth or other authentication is in use. + let authInfo: AuthInfo? + /// Information about the incoming HTTP request. + /// + /// Contains HTTP headers from the original request. Only available for + /// HTTP transports. This matches TypeScript SDK's `extra.requestInfo`. + let requestInfo: RequestInfo? + /// Closure to close the SSE stream for this request. + /// + /// Only set by HTTP transports with SSE support. + let closeSSEStream: (@Sendable () async -> Void)? + /// Closure to close the standalone SSE stream. + /// + /// Only set by HTTP transports with SSE support. + let closeStandaloneSSEStream: (@Sendable () async -> Void)? + } +} diff --git a/Sources/MCP/Server/Server.swift b/Sources/MCP/Server/Server.swift index 6ba1e27b..8297fdb3 100644 --- a/Sources/MCP/Server/Server.swift +++ b/Sources/MCP/Server/Server.swift @@ -112,6 +112,313 @@ public actor Server { self.tools = tools } } + + /// Context provided to request handlers for sending notifications during execution. + /// + /// When a request handler needs to send notifications (e.g., progress updates during + /// a long-running tool), it should use this context to ensure the notification is + /// routed to the correct client, even if other clients have connected in the meantime. + /// + /// This context provides: + /// - Request identification (`requestId`, `_meta`) + /// - Session tracking (`sessionId`) + /// - Authentication context (`authInfo`) + /// - Notification sending (`sendNotification`, `sendMessage`, `sendProgress`) + /// - Bidirectional requests (`elicit`, `elicitUrl`) + /// - Cancellation checking (`isCancelled`, `checkCancellation`) + /// - SSE stream management (`closeSSEStream`, `closeStandaloneSSEStream`) + /// + /// Example: + /// ```swift + /// server.withRequestHandler(CallTool.self) { params, context in + /// // Send progress notification using convenience method + /// try await context.sendProgress( + /// token: progressToken, + /// progress: 50.0, + /// total: 100.0, + /// message: "Processing..." + /// ) + /// // ... do work ... + /// return result + /// } + /// ``` + public struct RequestHandlerContext: Sendable { + /// Send a notification without parameters to the client that initiated this request. + /// + /// The notification will be routed to the correct client even if other clients + /// have connected since the request was received. + /// + /// - Parameter notification: The notification to send (for notifications without parameters) + public let sendNotification: @Sendable (any Notification) async throws -> Void + + /// Send a notification message with parameters to the client that initiated this request. + /// + /// Use this method to send notifications that have parameters, such as `ProgressNotification` + /// or `LogMessageNotification`. + /// + /// Example: + /// ```swift + /// try await context.sendMessage(ProgressNotification.message(.init( + /// progressToken: token, + /// progress: 50.0, + /// total: 100.0, + /// message: "Halfway done" + /// ))) + /// ``` + /// + /// - Parameter message: The notification message to send + public let sendMessage: @Sendable (any NotificationMessageProtocol) async throws -> Void + + /// Send raw data to the client that initiated this request. + /// + /// This is used internally for sending queued task messages (such as elicitation + /// or sampling requests that were queued during task execution). + /// + /// - Important: This is an internal API primarily used by the task system. + /// + /// - Parameter data: The raw JSON data to send + public let sendData: @Sendable (Data) async throws -> Void + + /// The session identifier for the client that initiated this request. + /// + /// For HTTP transports with multiple concurrent clients, each client session + /// has a unique identifier. This can be used for per-session features like + /// independent log levels. + /// + /// For simple transports (stdio, single-connection), this is `nil`. + public let sessionId: String? + + /// The JSON-RPC ID of the request being handled. + /// + /// This can be useful for tracking, logging, or correlating messages. + /// It matches the TypeScript SDK's `extra.requestId`. + public let requestId: ID + + /// The request metadata from the `_meta` field, if present. + /// + /// Contains metadata like the progress token for progress notifications. + /// This matches the TypeScript SDK's `extra._meta` and Python's `ctx.meta`. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { request, context in + /// if let progressToken = context._meta?.progressToken { + /// try await context.sendProgress(token: progressToken, progress: 50, total: 100) + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + public let _meta: RequestMeta? + + /// The task ID for task-augmented requests, if present. + /// + /// This is a convenience property that extracts the task ID from the + /// `_meta["io.modelcontextprotocol/related-task"]` field. + /// + /// This matches the TypeScript SDK's `extra.taskId`. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { params, context in + /// if let taskId = context.taskId { + /// print("Handling request as part of task: \(taskId)") + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + public var taskId: String? { + _meta?.relatedTaskId + } + + /// Authentication information for this request. + /// + /// Contains validated access token information when using HTTP transports + /// with OAuth or other token-based authentication. + /// + /// This matches the TypeScript SDK's `extra.authInfo`. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { params, context in + /// if let authInfo = context.authInfo { + /// print("Authenticated as: \(authInfo.clientId)") + /// print("Scopes: \(authInfo.scopes)") + /// + /// // Check if token has required scope + /// guard authInfo.scopes.contains("tools:execute") else { + /// throw MCPError.invalidRequest("Missing required scope") + /// } + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + public let authInfo: AuthInfo? + + /// Information about the incoming HTTP request. + /// + /// Contains HTTP headers from the original request. Only available for + /// HTTP transports. + /// + /// This matches the TypeScript SDK's `extra.requestInfo`. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { params, context in + /// if let requestInfo = context.requestInfo { + /// // Access custom headers + /// if let apiVersion = requestInfo.header("X-API-Version") { + /// print("Client API version: \(apiVersion)") + /// } + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + public let requestInfo: RequestInfo? + + /// Send a request to the client and wait for a response. + /// + /// This enables bidirectional communication from within a request handler, + /// allowing servers to request information from the client (e.g., elicitation, + /// sampling) during request processing. + /// + /// This matches the TypeScript SDK's `extra.sendRequest()` functionality. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { request, context in + /// // Request user input via elicitation + /// let result = try await context.elicit( + /// message: "Please confirm the operation", + /// requestedSchema: ElicitationSchema(properties: [ + /// "confirm": .boolean(BooleanSchema(title: "Confirm")) + /// ]) + /// ) + /// + /// if result.action == .accept { + /// // Process confirmed action + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + let sendRequest: @Sendable (Data) async throws -> Value + + // MARK: - Convenience Methods + + /// Send a progress notification to the client. + /// + /// Use this to report progress on long-running operations. + /// + /// - Parameters: + /// - token: The progress token from the request's `_meta.progressToken` + /// - progress: The current progress value (should increase monotonically) + /// - total: The total progress value, if known + /// - message: An optional human-readable message describing current progress + public func sendProgress( + token: ProgressToken, + progress: Double, + total: Double? = nil, + message: String? = nil + ) async throws { + try await sendMessage(ProgressNotification.message(.init( + progressToken: token, + progress: progress, + total: total, + message: message + ))) + } + + /// Send a resource list changed notification to the client. + /// + /// Call this when the list of available resources has changed. + public func sendResourceListChanged() async throws { + try await sendNotification(ResourceListChangedNotification()) + } + + /// Send a resource updated notification to the client. + /// + /// Call this when a specific resource's content has been updated. + /// + /// - Parameter uri: The URI of the resource that was updated + public func sendResourceUpdated(uri: String) async throws { + try await sendMessage(ResourceUpdatedNotification.message(.init(uri: uri))) + } + + /// Send a tool list changed notification to the client. + /// + /// Call this when the list of available tools has changed. + public func sendToolListChanged() async throws { + try await sendNotification(ToolListChangedNotification()) + } + + /// Send a prompt list changed notification to the client. + /// + /// Call this when the list of available prompts has changed. + public func sendPromptListChanged() async throws { + try await sendNotification(PromptListChangedNotification()) + } + + // MARK: - Cancellation Checking + + /// Whether the request has been cancelled. + /// + /// Check this property periodically during long-running operations + /// to respond to cancellation requests from the client. + /// + /// This returns `true` when: + /// - The client sends a `CancelledNotification` for this request + /// - The server is shutting down + /// + /// When cancelled, the handler should clean up resources and return + /// or throw an error. Per MCP spec, responses are not sent for cancelled requests. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { params, context in + /// for item in largeDataset { + /// // Check cancellation periodically + /// guard !context.isCancelled else { + /// throw CancellationError() + /// } + /// try await process(item) + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + public var isCancelled: Bool { + Task.isCancelled + } + + /// Check if the request has been cancelled and throw if so. + /// + /// Call this method periodically during long-running operations. + /// If the request has been cancelled, this throws `CancellationError`. + /// + /// This is equivalent to checking `isCancelled` and throwing manually, + /// but provides a more idiomatic Swift concurrency pattern. + /// + /// ## Example + /// + /// ```swift + /// server.withRequestHandler(CallTool.self) { params, context in + /// for item in largeDataset { + /// try context.checkCancellation() // Throws if cancelled + /// try await process(item) + /// } + /// return CallTool.Result(content: [.text("Done")]) + /// } + /// ``` + /// + /// - Throws: `CancellationError` if the request has been cancelled. + public func checkCancellation() throws { + try Task.checkCancellation() + } + } /// Server information private let serverInfo: Server.Info diff --git a/Sources/MCP/Server/Tools.swift b/Sources/MCP/Server/Tools.swift index fd10d934..490f0246 100644 --- a/Sources/MCP/Server/Tools.swift +++ b/Sources/MCP/Server/Tools.swift @@ -7,14 +7,49 @@ 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 + /// The tool name (intended for programmatic or logical use) public let name: String + /// A human-readable title for the tool, intended for UI display. + /// If not provided, the `annotations.title` or `name` should be used for display. + public let title: String? /// The tool description public let description: String? /// The tool input schema public let inputSchema: Value + /// An optional JSON Schema object defining the structure of the tool's output + /// returned in the `structuredContent` field of a `CallTool.Result`. + public let outputSchema: Value? + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + + /// Optional icons representing this tool. + public var icons: [Icon]? + + /// Execution-related properties for a tool. + public struct Execution: Hashable, Codable, Sendable { + /// The tool's preference for task-augmented execution. + public enum TaskSupport: String, Hashable, Codable, Sendable { + /// Clients MUST invoke the tool as a task + case required + /// Clients MAY invoke the tool as a task or normal request + case optional + /// Clients MUST NOT attempt to invoke the tool as a task (default) + case forbidden + } + + /// Indicates the tool's preference for task-augmented execution. + /// If not present, defaults to "forbidden". + public var taskSupport: TaskSupport? + + public init(taskSupport: TaskSupport? = nil) { + self.taskSupport = taskSupport + } + } + + /// Execution-related properties for the tool. + public var execution: Execution? /// Annotations that provide display-facing and operational information for a Tool. /// @@ -85,37 +120,72 @@ public struct Tool: Hashable, Codable, Sendable { /// Initialize a tool with a name, description, input schema, and annotations public init( name: String, - description: String?, + title: String? = nil, + description: String? = nil, inputSchema: Value, + outputSchema: Value? = nil, + _meta: [String: Value]? = nil, + icons: [Icon]? = nil, + execution: Execution? = nil, annotations: Annotations = nil ) { self.name = name + self.title = title self.description = description self.inputSchema = inputSchema + self.outputSchema = outputSchema + self._meta = _meta + self.icons = icons + self.execution = execution self.annotations = annotations } /// Content types that can be returned by a tool public enum Content: Hashable, Codable, Sendable { + /// Type alias for content-level annotations (with audience, priority, lastModified). + /// Not to be confused with `Tool.Annotations` which are tool-specific hints. + public typealias ContentAnnotations = MCP.Annotations + /// Text content - case text(String) + case text(String, annotations: ContentAnnotations?, _meta: [String: Value]?) /// Image content - case image(data: String, mimeType: String, metadata: [String: String]?) + case image(data: String, mimeType: String, annotations: ContentAnnotations?, _meta: [String: Value]?) /// Audio content - case audio(data: String, mimeType: String) - /// Embedded resource content - case resource(uri: String, mimeType: String, text: String?) + case audio(data: String, mimeType: String, annotations: ContentAnnotations?, _meta: [String: Value]?) + /// Embedded resource content (includes actual content) + case resource(resource: Resource.Content, annotations: ContentAnnotations?, _meta: [String: Value]?) + /// Resource link (reference to a resource that can be read) + case resourceLink(ResourceLink) + + // MARK: - Convenience initializers (backwards compatibility) + + /// Creates text content + public static func text(_ text: String) -> Content { + .text(text, annotations: nil, _meta: nil) + } + + /// Creates image content + public static func image(data: String, mimeType: String) -> Content { + .image(data: data, mimeType: mimeType, annotations: nil, _meta: nil) + } + + /// Creates audio content + public static func audio(data: String, mimeType: String) -> Content { + .audio(data: data, mimeType: mimeType, annotations: nil, _meta: nil) + } + + /// Creates embedded resource content with text + public static func resource(uri: String, mimeType: String? = nil, text: String) -> Content { + .resource(resource: .text(text, uri: uri, mimeType: mimeType), annotations: nil, _meta: nil) + } + + /// Creates embedded resource content with binary data + public static func resource(uri: String, mimeType: String? = nil, blob: Data) -> Content { + .resource(resource: .binary(blob, uri: uri, mimeType: mimeType), annotations: nil, _meta: nil) + } private enum CodingKeys: String, CodingKey { - case type - case text - case image - case resource - case audio - case uri - case mimeType - case data - case metadata + case type, text, data, mimeType, resource, annotations, _meta } public init(from decoder: Decoder) throws { @@ -125,22 +195,29 @@ 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(ContentAnnotations.self, forKey: .annotations) + let meta = try container.decodeIfPresent([String: Value].self, forKey: ._meta) + self = .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 metadata = try container.decodeIfPresent( - [String: String].self, forKey: .metadata) - self = .image(data: data, mimeType: mimeType, metadata: metadata) + let annotations = try container.decodeIfPresent(ContentAnnotations.self, forKey: .annotations) + let meta = try container.decodeIfPresent([String: Value].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(ContentAnnotations.self, forKey: .annotations) + let meta = try container.decodeIfPresent([String: Value].self, forKey: ._meta) + self = .audio(data: data, mimeType: mimeType, annotations: annotations, _meta: meta) 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) - self = .resource(uri: uri, mimeType: mimeType, text: text) + let resourceContent = try container.decode(Resource.Content.self, forKey: .resource) + let annotations = try container.decodeIfPresent(ContentAnnotations.self, forKey: .annotations) + let meta = try container.decodeIfPresent([String: Value].self, forKey: ._meta) + self = .resource(resource: resourceContent, annotations: annotations, _meta: meta) + case "resource_link": + let link = try ResourceLink(from: decoder) + self = .resourceLink(link) default: throw DecodingError.dataCorruptedError( forKey: .type, in: container, debugDescription: "Unknown tool content type") @@ -151,48 +228,69 @@ 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: .metadata) - 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) - case .resource(let uri, let mimeType, let text): + 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(uri, forKey: .uri) - try container.encode(mimeType, forKey: .mimeType) - try container.encodeIfPresent(text, forKey: .text) + try container.encode(resourceContent, forKey: .resource) + try container.encodeIfPresent(annotations, forKey: .annotations) + try container.encodeIfPresent(meta, forKey: ._meta) + case .resourceLink(let link): + try link.encode(to: encoder) } } } private enum CodingKeys: String, CodingKey { case name + case title case description case inputSchema + case outputSchema + case _meta + case icons + case execution case annotations } 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) - annotations = - try container.decodeIfPresent(Tool.Annotations.self, forKey: .annotations) ?? .init() + outputSchema = try container.decodeIfPresent(Value.self, forKey: .outputSchema) + _meta = try container.decodeIfPresent([String: Value].self, forKey: ._meta) + icons = try container.decodeIfPresent([Icon].self, forKey: .icons) + execution = try container.decodeIfPresent(Tool.Execution.self, forKey: .execution) + annotations = try container.decodeIfPresent(Tool.Annotations.self, forKey: .annotations) ?? .init() } public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(name, forKey: .name) - try container.encode(description, forKey: .description) + try container.encodeIfPresent(title, forKey: .title) + try container.encodeIfPresent(description, forKey: .description) try container.encode(inputSchema, forKey: .inputSchema) + try container.encodeIfPresent(outputSchema, forKey: .outputSchema) + try container.encodeIfPresent(_meta, forKey: ._meta) + try container.encodeIfPresent(icons, forKey: .icons) + try container.encodeIfPresent(execution, forKey: .execution) if !annotations.isEmpty { try container.encode(annotations, forKey: .annotations) } @@ -208,23 +306,52 @@ public enum ListTools: Method { public struct Parameters: NotRequired, Hashable, Codable, Sendable { public let cursor: String? + /// Request metadata including progress token. + public var _meta: RequestMeta? public init() { self.cursor = nil + self._meta = nil } - public init(cursor: String) { + public init(cursor: String? = nil, _meta: RequestMeta? = nil) { self.cursor = cursor + self._meta = _meta } } public struct Result: Hashable, Codable, Sendable { public let tools: [Tool] public let nextCursor: String? - - public init(tools: [Tool], nextCursor: String? = nil) { + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + + public init( + tools: [Tool], + nextCursor: String? = nil, + _meta: [String: Value]? = nil, + ) { self.tools = tools self.nextCursor = nextCursor + self._meta = _meta + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case tools, nextCursor, _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([String: Value].self, forKey: ._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) } } } @@ -237,21 +364,63 @@ public enum CallTool: Method { public struct Parameters: Hashable, Codable, Sendable { public let name: String public let arguments: [String: Value]? - - public init(name: String, arguments: [String: Value]? = nil) { + /// Request metadata including progress token. + public var _meta: RequestMeta? + + public init( + name: String, + arguments: [String: Value]? = nil, + _meta: RequestMeta? = nil + ) { self.name = name self.arguments = arguments + self._meta = _meta } } public struct Result: Hashable, Codable, Sendable { public let content: [Tool.Content] + /// An optional JSON object that represents the structured result of the tool call. + /// If the tool defined an `outputSchema`, this should conform to that schema. + // TODO: Add server-side output validation against the tool's outputSchema. + // TypeScript and Python SDKs validate structuredContent against outputSchema + // after the handler returns. This requires a tool cache to look up the schema. + public let structuredContent: Value? + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? public let isError: Bool? - public init(content: [Tool.Content], isError: Bool? = nil) { + public init( + content: [Tool.Content], + structuredContent: Value? = nil, + _meta: [String: Value]? = nil, + isError: Bool? = nil + ) { self.content = content + self.structuredContent = structuredContent + self._meta = _meta self.isError = isError } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case content, structuredContent, _meta, isError + } + + 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) + _meta = try container.decodeIfPresent([String: Value].self, forKey: ._meta) + isError = try container.decodeIfPresent(Bool.self, forKey: .isError) + } + + 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(_meta, forKey: ._meta) + try container.encodeIfPresent(isError, forKey: .isError) + } } } From 974d85fb56bf8dde68373921d9aa755f78797cfa Mon Sep 17 00:00:00 2001 From: Yehor Sobko Date: Fri, 16 Jan 2026 21:24:33 +0100 Subject: [PATCH 2/2] feat(Tool): intermediate updates to the Resources before migration to latest protocol version --- Sources/MCP/Server/Resources.swift | 218 ++++++++++++++++++++++++++--- Sources/MCP/Server/Server.swift | 16 +-- 2 files changed, 204 insertions(+), 30 deletions(-) diff --git a/Sources/MCP/Server/Resources.swift b/Sources/MCP/Server/Resources.swift index 0be14553..b04571ed 100644 --- a/Sources/MCP/Server/Resources.swift +++ b/Sources/MCP/Server/Resources.swift @@ -6,31 +6,48 @@ 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 title for the resource, intended for UI display. + /// If not provided, the `name` should be used for display. + public var title: String? /// The resource URI public var uri: String /// The resource description public var description: String? /// The resource MIME type public var mimeType: String? - /// The resource metadata - public var metadata: [String: String]? + /// The size of the raw resource content, in bytes, if known. + public var size: Int? + /// Optional annotations for the client. + public var annotations: Annotations? + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + /// Optional icons representing this resource. + public var icons: [Icon]? public init( name: String, + title: String? = nil, uri: String, description: String? = nil, mimeType: String? = nil, - metadata: [String: String]? = nil + size: Int? = nil, + annotations: Annotations? = nil, + _meta: [String: Value]? = nil, + icons: [Icon]? = nil ) { self.name = name + self.title = title self.uri = uri self.description = description self.mimeType = mimeType - self.metadata = metadata + self.size = size + self.annotations = annotations + self._meta = _meta + self.icons = icons } /// Content of a resource. @@ -43,7 +60,9 @@ public struct Resource: Hashable, Codable, Sendable { public let text: String? /// The resource binary content public let blob: String? - + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + public static func text(_ content: String, uri: String, mimeType: String? = nil) -> Self { .init(uri: uri, mimeType: mimeType, text: content) } @@ -57,6 +76,7 @@ public struct Resource: Hashable, Codable, Sendable { self.mimeType = mimeType self.text = text self.blob = nil + self._meta = nil } private init(uri: String, mimeType: String? = nil, blob: String) { @@ -64,30 +84,77 @@ public struct Resource: Hashable, Codable, Sendable { self.mimeType = mimeType self.text = nil self.blob = blob + self._meta = nil } } - /// A resource template. + /// A resource template that can generate multiple resources via URI pattern matching. + /// + /// Resource templates use [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/html/rfc6570) + /// to define patterns for dynamic resource URIs. Clients can use these templates to construct + /// resource URIs by substituting template variables. + /// + /// ## Example + /// + /// ```swift + /// // Define a template for user profiles + /// let template = Resource.Template( + /// uriTemplate: "users://{userId}/profile", + /// name: "user_profile", + /// title: "User Profile", + /// description: "Profile information for a specific user", + /// mimeType: "application/json" + /// ) + /// + /// // Register with a server + /// server.registerResources { + /// listTemplates: { _ in [template] }, + /// read: { uri in + /// // Parse userId from URI and return profile data + /// let userId = parseUserId(from: uri) + /// return [.text(getProfile(userId), uri: uri)] + /// } + /// } + /// ``` + /// + /// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/schema#resourcetemplate public struct Template: Hashable, Codable, Sendable { - /// The URI template pattern + /// The URI template pattern (RFC 6570 format, e.g., "file:///{path}"). public var uriTemplate: String /// The template name public var name: String - /// The template description + /// A human-readable title for the template, intended for UI display. + /// If not provided, the `name` should be used for display. + public var title: String? + /// A description of what resources this template provides. public var description: String? - /// The resource MIME type + /// The MIME type of resources generated from this template. public var mimeType: String? + /// Optional annotations for the client. + public var annotations: Annotations? + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + /// Optional icons representing this resource template. + public var icons: [Icon]? public init( uriTemplate: String, name: String, + title: String? = nil, description: String? = nil, - mimeType: String? = nil + mimeType: String? = nil, + annotations: Annotations? = nil, + _meta: [String: Value]? = nil, + icons: [Icon]? = nil ) { self.uriTemplate = uriTemplate self.name = name + self.title = title self.description = description self.mimeType = mimeType + self.annotations = annotations + self._meta = _meta + self.icons = icons } } } @@ -201,23 +268,52 @@ public enum ListResources: Method { public struct Parameters: NotRequired, Hashable, Codable, Sendable { public let cursor: String? + /// Request metadata including progress token. + public var _meta: RequestMeta? public init() { self.cursor = nil + self._meta = nil } - public init(cursor: String) { + public init(cursor: String? = nil, _meta: RequestMeta? = nil) { self.cursor = cursor + self._meta = _meta } } public struct Result: Hashable, Codable, Sendable { public let resources: [Resource] public let nextCursor: String? + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? - public init(resources: [Resource], nextCursor: String? = nil) { + public init( + resources: [Resource], + nextCursor: String? = nil, + _meta: [String: Value]? = nil + ) { self.resources = resources self.nextCursor = nextCursor + self._meta = _meta + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case resources, nextCursor, _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([String: Value].self, forKey: ._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) } } } @@ -229,17 +325,42 @@ public enum ReadResource: Method { public struct Parameters: Hashable, Codable, Sendable { public let uri: String + /// Request metadata including progress token. + public var _meta: RequestMeta? - public init(uri: String) { + public init(uri: String, _meta: RequestMeta? = nil) { self.uri = uri + self._meta = _meta } } public struct Result: Hashable, Codable, Sendable { public let contents: [Resource.Content] + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? - public init(contents: [Resource.Content]) { + public init( + contents: [Resource.Content], + _meta: [String: Value]? = nil, + ) { self.contents = contents + self._meta = _meta + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case contents, _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([String: Value].self, forKey: ._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) } } } @@ -251,28 +372,54 @@ public enum ListResourceTemplates: Method { public struct Parameters: NotRequired, Hashable, Codable, Sendable { public let cursor: String? + /// Request metadata including progress token. + public var _meta: RequestMeta? public init() { self.cursor = nil + self._meta = nil } - public init(cursor: String) { + public init(cursor: String? = nil, _meta: RequestMeta? = nil) { self.cursor = cursor + self._meta = _meta } } public struct Result: Hashable, Codable, Sendable { public let templates: [Resource.Template] public let nextCursor: String? - - public init(templates: [Resource.Template], nextCursor: String? = nil) { + /// Reserved for clients and servers to attach additional metadata. + public var _meta: [String: Value]? + + public init( + templates: [Resource.Template], + nextCursor: String? = nil, + _meta: [String: Value]? = nil, + ) { self.templates = templates self.nextCursor = nextCursor + self._meta = _meta } private enum CodingKeys: String, CodingKey { case templates = "resourceTemplates" case nextCursor + case _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([String: Value].self, forKey: ._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) } } } @@ -286,27 +433,54 @@ 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://modelcontextprotocol.io/specification/2025-11-25/server/resources#subscriptions public enum ResourceSubscribe: Method { public static let name: String = "resources/subscribe" public struct Parameters: Hashable, Codable, Sendable { public let uri: String + /// Request metadata including progress token. + public var _meta: RequestMeta? + + public init(uri: String, _meta: RequestMeta? = nil) { + self.uri = uri + self._meta = _meta + } } public typealias Result = Empty } +/// Clients can unsubscribe from resources to stop receiving update notifications. +/// - SeeAlso: https://modelcontextprotocol.io/specification/2025-11-25/server/resources/#subscriptions +public enum ResourceUnsubscribe: Method { + public static let name: String = "resources/unsubscribe" + + public struct Parameters: Hashable, Codable, Sendable { + public let uri: String + /// Request metadata including progress token. + public var _meta: RequestMeta? + + public init(uri: String, _meta: RequestMeta? = nil) { + self.uri = uri + self._meta = _meta + } + } +} + /// 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 public struct ResourceUpdatedNotification: Notification { public static let name: String = "notifications/resources/updated" - + public struct Parameters: Hashable, Codable, Sendable { public let uri: String - - public init(uri: String) { + /// Reserved for additional metadata. + public var _meta: [String: Value]? + + public init(uri: String, _meta: [String: Value]? = nil) { self.uri = uri + self._meta = _meta } } } diff --git a/Sources/MCP/Server/Server.swift b/Sources/MCP/Server/Server.swift index 8297fdb3..07a3b241 100644 --- a/Sources/MCP/Server/Server.swift +++ b/Sources/MCP/Server/Server.swift @@ -130,7 +130,7 @@ public actor Server { /// /// Example: /// ```swift - /// server.withRequestHandler(CallTool.self) { params, context in + /// server.withMethodHandler(CallTool.self) { params, context in /// // Send progress notification using convenience method /// try await context.sendProgress( /// token: progressToken, @@ -202,7 +202,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { request, context in + /// server.withMethodHandler(CallTool.self) { request, context in /// if let progressToken = context._meta?.progressToken { /// try await context.sendProgress(token: progressToken, progress: 50, total: 100) /// } @@ -221,7 +221,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { params, context in + /// server.withMethodHandler(CallTool.self) { params, context in /// if let taskId = context.taskId { /// print("Handling request as part of task: \(taskId)") /// } @@ -242,7 +242,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { params, context in + /// server.withMethodHandler(CallTool.self) { params, context in /// if let authInfo = context.authInfo { /// print("Authenticated as: \(authInfo.clientId)") /// print("Scopes: \(authInfo.scopes)") @@ -267,7 +267,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { params, context in + /// server.withMethodHandler(CallTool.self) { params, context in /// if let requestInfo = context.requestInfo { /// // Access custom headers /// if let apiVersion = requestInfo.header("X-API-Version") { @@ -290,7 +290,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { request, context in + /// server.withMethodHandler(CallTool.self) { request, context in /// // Request user input via elicitation /// let result = try await context.elicit( /// message: "Please confirm the operation", @@ -379,7 +379,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { params, context in + /// server.withMethodHandler(CallTool.self) { params, context in /// for item in largeDataset { /// // Check cancellation periodically /// guard !context.isCancelled else { @@ -405,7 +405,7 @@ public actor Server { /// ## Example /// /// ```swift - /// server.withRequestHandler(CallTool.self) { params, context in + /// server.withMethodHandler(CallTool.self) { params, context in /// for item in largeDataset { /// try context.checkCancellation() // Throws if cancelled /// try await process(item)