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:
yehorsobko 2026-03-03 16:33:28 +01:00 • committed by GitHub
commit 973b46b8f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 692 additions and 507 deletions

View file

@ -25,6 +25,12 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Prepare Linux apt dependencies
if: matrix.os == 'ubuntu-latest'
run: |
sudo -n apt-get update
sudo -n apt-get install -y --no-install-recommends libcurl4-openssl-dev
- name: Setup Swift on Linux - name: Setup Swift on Linux
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
uses: vapor/swiftly-action@v0.2 uses: vapor/swiftly-action@v0.2

View file

@ -1,5 +1,5 @@
{ {
"originHash" : "06a30e0a3f4c69c306d3b14f13c2b4b3964674139bfeec9b920a2bc3d5b1ca20", "originHash" : "2e844b5aa785005b516db94bbdf89be795d57d958f648d3bf655b75ad9ad9d68",
"pins" : [ "pins" : [
{ {
"identity" : "eventsource", "identity" : "eventsource",
@ -10,15 +10,6 @@
"version" : "1.1.0" "version" : "1.1.0"
} }
}, },
{
"identity" : "swift-async-algorithms",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-async-algorithms.git",
"state" : {
"revision" : "6c050d5ef8e1aa6342528460db614e9770d7f804",
"version" : "1.1.1"
}
},
{ {
"identity" : "swift-atomics", "identity" : "swift-atomics",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View file

@ -3,28 +3,6 @@
import PackageDescription import PackageDescription
// Base dependencies needed on all platforms
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/apple/swift-system.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
.package(url: "https://github.com/apple/swift-async-algorithms.git", from: "1.0.0"),
.package(url: "https://github.com/mattt/eventsource.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
]
// Target dependencies needed on all platforms
var targetDependencies: [Target.Dependency] = [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
.product(
name: "EventSource", package: "eventsource",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .visionOS, .watchOS, .macCatalyst])),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
]
let package = Package( let package = Package(
name: "mcp-swift-sdk", name: "mcp-swift-sdk",
platforms: [ platforms: [
@ -47,27 +25,55 @@ let package = Package(
name: "mcp-everything-client", name: "mcp-everything-client",
targets: ["MCPConformanceClient"]) targets: ["MCPConformanceClient"])
], ],
dependencies: dependencies, dependencies: [
.package(url: "https://github.com/apple/swift-system.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
.package(url: "https://github.com/mattt/eventsource.git", from: "1.1.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
],
targets: [ targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target( .target(
name: "MCP", name: "MCP",
dependencies: targetDependencies, dependencies: [
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
.product(
name: "EventSource", package: "eventsource",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .visionOS, .watchOS, .macCatalyst])),
],
swiftSettings: [ swiftSettings: [
.enableUpcomingFeature("StrictConcurrency") .enableUpcomingFeature("StrictConcurrency")
] ]
), ),
.testTarget( .testTarget(
name: "MCPTests", name: "MCPTests",
dependencies: ["MCP"] + targetDependencies), dependencies: [
"MCP",
.product(name: "SystemPackage", package: "swift-system"),
.product(name: "Logging", package: "swift-log"),
.product(
name: "EventSource", package: "eventsource",
condition: .when(platforms: [.macOS, .iOS, .tvOS, .visionOS, .watchOS, .macCatalyst])),
]
),
.executableTarget( .executableTarget(
name: "MCPConformanceServer", name: "MCPConformanceServer",
dependencies: ["MCP"] + targetDependencies, dependencies: [
path: "Sources/MCPConformance/Server"), "MCP",
.product(name: "Logging", package: "swift-log"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOHTTP1", package: "swift-nio"),
],
path: "Sources/MCPConformance/Server"
),
.executableTarget( .executableTarget(
name: "MCPConformanceClient", name: "MCPConformanceClient",
dependencies: ["MCP"] + targetDependencies, dependencies: [
path: "Sources/MCPConformance/Client") "MCP",
.product(name: "Logging", package: "swift-log"),
],
path: "Sources/MCPConformance/Client"
)
] ]
) )

View file

@ -171,7 +171,6 @@ extension MCPError: Codable {
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(code, forKey: .code) try container.encode(code, forKey: .code)
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
// Encode additional data if available // Encode additional data if available
switch self { switch self {
@ -180,13 +179,17 @@ extension MCPError: Codable {
.methodNotFound(let detail), .methodNotFound(let detail),
.invalidParams(let detail), .invalidParams(let detail),
.internalError(let detail): .internalError(let detail):
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
if let detail = detail { if let detail = detail {
try container.encode(["detail": detail], forKey: .data) try container.encode(["detail": detail], forKey: .data)
} }
case .serverError(_, _): case .serverError(_, _):
// No additional data for server errors // No additional data for server errors
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
break break
case .urlElicitationRequired(_, let elicitations): case .urlElicitationRequired(let message, let elicitations):
// Encode the raw message so decode can round-trip without prefix doubling
try container.encode(message, forKey: .message)
// Encode elicitations array as structured data // Encode elicitations array as structured data
let elicitationsData = elicitations.map { info -> [String: Value] in let elicitationsData = elicitations.map { info -> [String: Value] in
return [ return [
@ -201,8 +204,9 @@ extension MCPError: Codable {
forKey: .data forKey: .data
) )
case .connectionClosed: case .connectionClosed:
break try container.encode(errorDescription ?? "Unknown error", forKey: .message)
case .transportError(let error): case .transportError(let error):
try container.encode(errorDescription ?? "Unknown error", forKey: .message)
try container.encode( try container.encode(
["error": error.localizedDescription], ["error": error.localizedDescription],
forKey: .data forKey: .data
@ -279,7 +283,21 @@ extension MCPError: Codable {
extension MCPError: Equatable { extension MCPError: Equatable {
public static func == (lhs: MCPError, rhs: MCPError) -> Bool { public static func == (lhs: MCPError, rhs: MCPError) -> Bool {
lhs.code == rhs.code switch (lhs, rhs) {
case (.parseError(let a), .parseError(let b)): return a == b
case (.invalidRequest(let a), .invalidRequest(let b)): return a == b
case (.methodNotFound(let a), .methodNotFound(let b)): return a == b
case (.invalidParams(let a), .invalidParams(let b)): return a == b
case (.internalError(let a), .internalError(let b)): return a == b
case (.serverError(let c1, let m1), .serverError(let c2, let m2)):
return c1 == c2 && m1 == m2
case (.urlElicitationRequired(let m1, let e1), .urlElicitationRequired(let m2, let e2)):
return m1 == m2 && e1 == e2
case (.connectionClosed, .connectionClosed): return true
case (.transportError(let a), .transportError(let b)):
return a.localizedDescription == b.localizedDescription
default: return false
}
} }
} }

View file

@ -38,10 +38,9 @@ struct AnyMethod: Method, Sendable {
extension Method where Parameters == Empty { extension Method where Parameters == Empty {
public static func request( public static func request(
id: ID = .random, id: ID = .random
_meta: Metadata? = nil
) -> Request<Self> { ) -> Request<Self> {
Request(id: id, method: name, params: Empty(), _meta: _meta) Request(id: id, method: name, params: Empty())
} }
} }
@ -55,28 +54,25 @@ extension Method {
/// Create a request with the given parameters. /// Create a request with the given parameters.
public static func request( public static func request(
id: ID = .random, id: ID = .random,
_ parameters: Self.Parameters, _ parameters: Self.Parameters
_meta: Metadata? = nil
) -> Request<Self> { ) -> Request<Self> {
Request(id: id, method: name, params: parameters, _meta: _meta) Request(id: id, method: name, params: parameters)
} }
/// Create a response with the given result. /// Create a response with the given result.
public static func response( public static func response(
id: ID, id: ID,
result: Self.Result, result: Self.Result
_meta: Metadata? = nil
) -> Response<Self> { ) -> Response<Self> {
Response(id: id, result: result, _meta: _meta) Response(id: id, result: result)
} }
/// Create a response with the given error. /// Create a response with the given error.
public static func response( public static func response(
id: ID, id: ID,
error: MCPError, error: MCPError
_meta: Metadata? = nil
) -> Response<Self> { ) -> Response<Self> {
Response(id: id, error: error, _meta: _meta) Response(id: id, error: error)
} }
} }
@ -90,23 +86,19 @@ public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
public let method: String public let method: String
/// The request parameters. /// The request parameters.
public let params: M.Parameters public let params: M.Parameters
/// Metadata for this request (see spec for _meta usage, includes progressToken)
public let _meta: Metadata?
init( init(
id: ID = .random, id: ID = .random,
method: String, method: String,
params: M.Parameters, params: M.Parameters
_meta: Metadata? = nil
) { ) {
self.id = id self.id = id
self.method = method self.method = method
self.params = params self.params = params
self._meta = _meta
} }
private enum CodingKeys: String, CodingKey, CaseIterable { private enum CodingKeys: String, CodingKey, CaseIterable {
case jsonrpc, id, method, params, _meta case jsonrpc, id, method, params
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -115,7 +107,6 @@ public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
try container.encode(id, forKey: .id) try container.encode(id, forKey: .id)
try container.encode(method, forKey: .method) try container.encode(method, forKey: .method)
try container.encode(params, forKey: .params) try container.encode(params, forKey: .params)
try container.encodeIfPresent(_meta, forKey: ._meta)
} }
} }
@ -129,7 +120,6 @@ extension Request {
} }
id = try container.decode(ID.self, forKey: .id) id = try container.decode(ID.self, forKey: .id)
method = try container.decode(String.self, forKey: .method) method = try container.decode(String.self, forKey: .method)
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
if M.Parameters.self is NotRequired.Type { if M.Parameters.self is NotRequired.Type {
// For NotRequired parameters, use decodeIfPresent or init() // For NotRequired parameters, use decodeIfPresent or init()
@ -221,44 +211,37 @@ public struct Response<M: Method>: Hashable, Identifiable, Codable, Sendable {
public let id: ID public let id: ID
/// The response result. /// The response result.
public let result: Swift.Result<M.Result, MCPError> public let result: Swift.Result<M.Result, MCPError>
/// Metadata for this response (see spec for _meta usage)
public let _meta: Metadata?
public init( public init(
id: ID, id: ID,
result: Swift.Result<M.Result, MCPError>, result: Swift.Result<M.Result, MCPError>
_meta: Metadata? = nil
) { ) {
self.id = id self.id = id
self.result = result self.result = result
self._meta = _meta
} }
public init( public init(
id: ID, id: ID,
result: M.Result, result: M.Result
_meta: Metadata? = nil
) { ) {
self.init(id: id, result: .success(result), _meta: _meta) self.init(id: id, result: .success(result))
} }
public init( public init(
id: ID, id: ID,
error: MCPError, error: MCPError
_meta: Metadata? = nil
) { ) {
self.init(id: id, result: .failure(error), _meta: _meta) self.init(id: id, result: .failure(error))
} }
private enum CodingKeys: String, CodingKey, CaseIterable { private enum CodingKeys: String, CodingKey, CaseIterable {
case jsonrpc, id, result, error, _meta case jsonrpc, id, result, error
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(jsonrpc, forKey: .jsonrpc) try container.encode(jsonrpc, forKey: .jsonrpc)
try container.encode(id, forKey: .id) try container.encode(id, forKey: .id)
try container.encodeIfPresent(_meta, forKey: ._meta)
switch result { switch result {
case .success(let result): case .success(let result):
@ -286,7 +269,6 @@ public struct Response<M: Method>: Hashable, Identifiable, Codable, Sendable {
codingPath: container.codingPath, codingPath: container.codingPath,
debugDescription: "Invalid response")) debugDescription: "Invalid response"))
} }
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
} }
} }
@ -301,14 +283,12 @@ extension AnyResponse {
let resultValue = try JSONDecoder().decode(Value.self, from: data) let resultValue = try JSONDecoder().decode(Value.self, from: data)
self = Response<AnyMethod>( self = Response<AnyMethod>(
id: response.id, id: response.id,
result: .success(resultValue), result: .success(resultValue)
_meta: response._meta
) )
case .failure(let error): case .failure(let error):
self = Response<AnyMethod>( self = Response<AnyMethod>(
id: response.id, id: response.id,
result: .failure(error), result: .failure(error)
_meta: response._meta
) )
} }
} }

View file

@ -0,0 +1,43 @@
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
/// An error indicating a type mismatch when decoding a pending request response.
struct TypeMismatchError: Swift.Error {}
/// A pending request with a continuation for the result.
struct PendingRequest<T> {
let continuation: CheckedContinuation<T, Swift.Error>
}
/// A type-erased pending request.
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))
}
}

View file

@ -221,6 +221,14 @@ public actor HTTPClientTransport: Transport {
logger.debug("HTTP clienttransport disconnected") logger.debug("HTTP clienttransport disconnected")
} }
/// Updates the protocol version used for `MCP-Protocol-Version` headers on subsequent requests.
///
/// Call this after lifecycle negotiation if the server responds with a different version.
/// - Parameter version: The negotiated protocol version.
public func updateNegotiatedProtocolVersion(_ version: String) {
self.protocolVersion = version
}
/// Sends data through an HTTP POST request /// Sends data through an HTTP POST request
/// ///
/// This sends a JSON-RPC message to the server via HTTP POST and processes /// This sends a JSON-RPC message to the server via HTTP POST and processes

View file

@ -205,8 +205,14 @@ public struct SessionValidator: HTTPRequestValidator {
// Skip validation for initialization requests // Skip validation for initialization requests
guard !context.isInitializationRequest else { return nil } guard !context.isInitializationRequest else { return nil }
// If no session exists yet, skip (server hasn't been initialized) // Non-initialization requests require an established session.
guard let expectedSessionID = context.sessionID else { return nil } guard let expectedSessionID = context.sessionID else {
return .error(
statusCode: 400,
.invalidRequest("Bad Request: Session not initialized"),
sessionID: nil
)
}
let requestSessionID = request.header(HTTPHeaderName.sessionID) let requestSessionID = request.header(HTTPHeaderName.sessionID)

View file

@ -88,7 +88,7 @@ public enum HTTPResponse: Sendable {
/// Error response with a JSON-RPC error body. /// Error response with a JSON-RPC error body.
/// The status code, headers, and body are derived automatically. /// The status code, headers, and body are derived automatically.
case error(statusCode: Int, MCPError, sessionID: String? = nil) case error(statusCode: Int, MCPError, sessionID: String? = nil, extraHeaders: [String: String] = [:])
// MARK: - Computed Properties // MARK: - Computed Properties
@ -96,7 +96,7 @@ public enum HTTPResponse: Sendable {
switch self { switch self {
case .accepted: 202 case .accepted: 202
case .ok, .data, .stream: 200 case .ok, .data, .stream: 200
case .error(let code, _, _): code case .error(let code, _, _, _): code
} }
} }
@ -104,9 +104,10 @@ public enum HTTPResponse: Sendable {
switch self { switch self {
case .accepted(let headers), .ok(let headers), .data(_, let headers), .stream(_, let headers): case .accepted(let headers), .ok(let headers), .data(_, let headers), .stream(_, let headers):
return headers return headers
case .error(_, _, let sessionID): case .error(_, _, let sessionID, let extraHeaders):
var headers: [String: String] = [HTTPHeaderName.contentType: ContentType.json] var headers: [String: String] = [HTTPHeaderName.contentType: ContentType.json]
if let sessionID { headers[HTTPHeaderName.sessionID] = sessionID } if let sessionID { headers[HTTPHeaderName.sessionID] = sessionID }
headers.merge(extraHeaders) { _, new in new }
return headers return headers
} }
} }
@ -118,7 +119,7 @@ public enum HTTPResponse: Sendable {
return nil return nil
case .data(let data, _): case .data(let data, _):
return data return data
case .error(_, let error, _): case .error(_, let error, _, _):
let errorBody: [String: Any] = [ let errorBody: [String: Any] = [
"jsonrpc": "2.0", "jsonrpc": "2.0",
"error": [ "error": [
@ -136,8 +137,8 @@ public enum HTTPResponse: Sendable {
/// Standard header names used by the MCP Streamable HTTP transport. /// Standard header names used by the MCP Streamable HTTP transport.
public enum HTTPHeaderName { public enum HTTPHeaderName {
public static let sessionID = "Mcp-Session-Id" public static let sessionID = "MCP-Session-Id"
public static let protocolVersion = "Mcp-Protocol-Version" public static let protocolVersion = "MCP-Protocol-Version"
public static let lastEventID = "Last-Event-Id" public static let lastEventID = "Last-Event-Id"
public static let accept = "Accept" public static let accept = "Accept"
public static let contentType = "Content-Type" public static let contentType = "Content-Type"

View file

@ -182,7 +182,8 @@ public actor StatefulHTTPServerTransport: Transport {
return .error( return .error(
statusCode: 405, statusCode: 405,
.invalidRequest("Method Not Allowed"), .invalidRequest("Method Not Allowed"),
sessionID: sessionID sessionID: sessionID,
extraHeaders: [HTTPHeaderName.allow: "GET, POST, DELETE"]
) )
} }
} }
@ -238,6 +239,15 @@ public actor StatefulHTTPServerTransport: Transport {
} }
private func handleInitializationRequest(_ body: Data, request: HTTPRequest) -> HTTPResponse { private func handleInitializationRequest(_ body: Data, request: HTTPRequest) -> HTTPResponse {
// Reject re-initialization at the transport level
if sessionID != nil {
return .error(
statusCode: 400,
.invalidRequest("Bad Request: Session already initialized"),
sessionID: sessionID
)
}
// Generate session ID // Generate session ID
let newSessionID = sessionIDGenerator.generateSessionID() let newSessionID = sessionIDGenerator.generateSessionID()
@ -357,9 +367,7 @@ public actor StatefulHTTPServerTransport: Transport {
supportedProtocolVersions: Version.supported supportedProtocolVersions: Version.supported
) )
// Only run session validation for DELETE (not all validators) if let errorResponse = validationPipeline.validate(request, context: context) {
let sessionValidator = SessionValidator()
if let errorResponse = sessionValidator.validate(request, context: context) {
return errorResponse return errorResponse
} }
@ -481,14 +489,27 @@ public actor StatefulHTTPServerTransport: Transport {
continuation: AsyncThrowingStream<Data, Swift.Error>.Continuation, continuation: AsyncThrowingStream<Data, Swift.Error>.Continuation,
protocolVersion: String protocolVersion: String
) { ) {
// Priming events with empty data are only safe for clients >= 2025-11-25 // Priming events with empty data are only safe for clients >= 2025-03-26
guard protocolVersion >= "2025-11-25" else { return } guard protocolVersion >= "2025-03-26" else { return }
let primingEventID = storeEvent(streamID: streamID, message: nil) let primingEventID = storeEvent(streamID: streamID, message: nil)
let primingEvent = SSEEvent.priming(id: primingEventID, retry: retryInterval) let primingEvent = SSEEvent.priming(id: primingEventID, retry: retryInterval)
continuation.yield(primingEvent.formatted()) continuation.yield(primingEvent.formatted())
} }
private func extractProtocolVersion(from body: Data, request: HTTPRequest) -> String {
// For initialize requests, extract from the request body params
if let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any],
let method = json["method"] as? String, method == "initialize",
let params = json["params"] as? [String: Any],
let version = params["protocolVersion"] as? String
{
return version
}
// For other requests, use the header
return request.header(HTTPHeaderName.protocolVersion) ?? Version.latest
}
// MARK: - Session Helpers // MARK: - Session Helpers
private func sessionHeaders() -> [String: String] { private func sessionHeaders() -> [String: String] {
@ -504,17 +525,16 @@ public actor StatefulHTTPServerTransport: Transport {
return id.utf8.allSatisfy { $0 >= 0x21 && $0 <= 0x7E } return id.utf8.allSatisfy { $0 >= 0x21 && $0 <= 0x7E }
} }
private func extractProtocolVersion(from body: Data, request: HTTPRequest) -> String { // MARK: - Stream Control
// For initialize requests, extract from the request body params
if let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any], /// Closes the SSE stream for a specific request without sending a response.
let method = json["method"] as? String, method == "initialize", ///
let params = json["params"] as? [String: Any], /// Used to trigger client reconnection mid-call (SEP-1699). The response,
let version = params["protocolVersion"] as? String /// when eventually sent, will be stored and replayed to the reconnected stream.
{ package func closeSSEStream(forRequestID requestID: String) {
return version guard let continuation = requestSSEContinuations[requestID] else { return }
} continuation.finish()
// For other requests, use the header requestSSEContinuations.removeValue(forKey: requestID)
return request.header(HTTPHeaderName.protocolVersion) ?? Version.latest
} }
// MARK: - Termination // MARK: - Termination

View file

@ -162,7 +162,8 @@ public actor StatelessHTTPServerTransport: Transport {
default: default:
return .error( return .error(
statusCode: 405, statusCode: 405,
.invalidRequest("Method Not Allowed") .invalidRequest("Method Not Allowed"),
extraHeaders: [HTTPHeaderName.allow: "POST"]
) )
} }
} }

View file

@ -15,13 +15,13 @@ public struct CancelledNotification: Notification {
/// The ID of the request to cancel. /// The ID of the request to cancel.
/// ///
/// This MUST correspond to the ID of a request previously issued /// This MUST correspond to the ID of a request previously issued
/// in the same direction. /// in the same direction. Optional per the spec (`requestId?`).
public let requestId: ID public let requestId: ID?
/// An optional human-readable reason for the cancellation. /// An optional human-readable reason for the cancellation.
public let reason: String? public let reason: String?
public init(requestId: ID, reason: String? = nil) { public init(requestId: ID? = nil, reason: String? = nil) {
self.requestId = requestId self.requestId = requestId
self.reason = reason self.reason = reason
} }

View file

@ -38,11 +38,27 @@ public actor Client {
public var title: String? public var title: String?
/// The client version /// The client version
public var version: String public var version: String
/// Optional description of the client
public var description: String?
/// Optional website URL for the client
public var websiteUrl: String?
/// Optional set of sized icons for display in a user interface
public var 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.name = name
self.title = title self.title = title
self.version = version self.version = version
self.description = description
self.websiteUrl = websiteUrl
self.icons = icons
} }
} }
@ -163,46 +179,6 @@ public actor Client {
/// The task for the message handling loop /// The task for the message handling loop
private var task: Task<Void, Never>? private var task: Task<Void, Never>?
/// 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))
}
}
/// A dictionary of type-erased pending requests, keyed by request ID /// A dictionary of type-erased pending requests, keyed by request ID
private var pendingRequests: [ID: AnyPendingRequest] = [:] private var pendingRequests: [ID: AnyPendingRequest] = [:]
// Add reusable JSON encoder/decoder // Add reusable JSON encoder/decoder
@ -213,10 +189,15 @@ public actor Client {
name: String, name: String,
version: String, version: String,
title: String? = nil, title: String? = nil,
description: String? = nil,
websiteUrl: String? = nil,
icons: [Icon]? = nil,
capabilities: Capabilities = Capabilities(), capabilities: Capabilities = Capabilities(),
configuration: Configuration = .default configuration: Configuration = .default
) { ) {
self.clientInfo = Client.Info(name: name, version: version, title: title) self.clientInfo = Client.Info(
name: name, version: version, title: title,
description: description, websiteUrl: websiteUrl, icons: icons)
self.capabilities = capabilities self.capabilities = capabilities
self.configuration = configuration self.configuration = configuration
} }
@ -285,13 +266,15 @@ public actor Client {
await self.logger?.debug( await self.logger?.debug(
"Received cancellation notification", "Received cancellation notification",
metadata: [ metadata: [
"requestId": "\(requestId)", "requestId": requestId.map { "\($0)" } ?? "none",
"reason": reason.map { "\($0)" } ?? "none", "reason": reason.map { "\($0)" } ?? "none",
] ]
) )
// Remove the pending request and resume with cancellation error // Remove the pending request and resume with cancellation error
if let pendingRequest = await self.removePendingRequest(id: requestId) { if let requestId = requestId,
let pendingRequest = await self.removePendingRequest(id: requestId)
{
pendingRequest.resume(throwing: CancellationError()) pendingRequest.resume(throwing: CancellationError())
} }
} }
@ -671,6 +654,12 @@ public actor Client {
self.serverVersion = result.protocolVersion self.serverVersion = result.protocolVersion
self.instructions = result.instructions self.instructions = result.instructions
// For HTTP transport, ensure subsequent MCP-Protocol-Version headers
// reflect the negotiated lifecycle version.
if let httpTransport = connection as? HTTPClientTransport {
await httpTransport.updateNegotiatedProtocolVersion(result.protocolVersion)
}
try await notify(InitializedNotification.message()) try await notify(InitializedNotification.message())
return result return result
@ -683,7 +672,7 @@ public actor Client {
// MARK: - Prompts // MARK: - Prompts
public func getPrompt(name: String, arguments: [String: Value]? = nil) async throws public func getPrompt(name: String, arguments: [String: String]? = nil) async throws
-> (description: String?, messages: [Prompt.Message]) -> (description: String?, messages: [Prompt.Message])
{ {
try validateServerCapability(\.prompts, "Prompts") try validateServerCapability(\.prompts, "Prompts")
@ -923,7 +912,7 @@ public actor Client {
promptName: String, promptName: String,
argumentName: String, argumentName: String,
argumentValue: String, argumentValue: String,
context: [String: Value]? = nil context: [String: String]? = nil
) async throws -> Complete.Result.Completion { ) async throws -> Complete.Result.Completion {
try validateServerCapability(\.completions, "Completions") try validateServerCapability(\.completions, "Completions")
let request = Complete.request( let request = Complete.request(
@ -954,7 +943,7 @@ public actor Client {
resourceURI: String, resourceURI: String,
argumentName: String, argumentName: String,
argumentValue: String, argumentValue: String,
context: [String: Value]? = nil context: [String: String]? = nil
) async throws -> Complete.Result.Completion { ) async throws -> Complete.Result.Completion {
try validateServerCapability(\.completions, "Completions") try validateServerCapability(\.completions, "Completions")
let request = Complete.request( let request = Complete.request(

View file

@ -68,15 +68,15 @@ public enum CreateElicitation: Method {
public var message: String public var message: String
/// Elicitation mode (optional for backward compatibility, defaults to form) /// Elicitation mode (optional for backward compatibility, defaults to form)
public var mode: Elicitation.Mode? public var mode: Elicitation.Mode?
/// Optional schema describing the expected response content /// Schema describing the expected response content (required per spec)
public var requestedSchema: Elicitation.RequestSchema? public var requestedSchema: Elicitation.RequestSchema
/// Optional metadata /// Optional metadata
public var _meta: Metadata? public var _meta: Metadata?
public init( public init(
message: String, message: String,
mode: Elicitation.Mode? = nil, mode: Elicitation.Mode? = nil,
requestedSchema: Elicitation.RequestSchema? = nil, requestedSchema: Elicitation.RequestSchema = .init(),
_meta: Metadata? = nil _meta: Metadata? = nil
) { ) {
self.message = message self.message = message
@ -188,7 +188,7 @@ extension CreateElicitation.Parameters: Codable {
} else { } else {
// Form mode (default for backward compatibility) // Form mode (default for backward compatibility)
let message = try container.decode(String.self, forKey: .message) let message = try container.decode(String.self, forKey: .message)
let requestedSchema = try container.decodeIfPresent( let requestedSchema = try container.decode(
Elicitation.RequestSchema.self, forKey: .requestedSchema) Elicitation.RequestSchema.self, forKey: .requestedSchema)
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta) let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
self = .form(FormParameters( self = .form(FormParameters(

View file

@ -16,30 +16,37 @@ public struct Root: Hashable, Codable, Sendable {
public let uri: String public let uri: String
/// Optional human-readable name for the root /// Optional human-readable name for the root
public let name: String? public let name: String?
/// Optional metadata
public var _meta: Metadata?
public init( public init(
uri: String, uri: String,
name: String? = nil name: String? = nil,
_meta: Metadata? = nil
) { ) {
self.uri = uri self.uri = uri
self.name = name self.name = name
self._meta = _meta
} }
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case uri case uri
case name case name
case _meta
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
uri = try container.decode(String.self, forKey: .uri) uri = try container.decode(String.self, forKey: .uri)
name = try container.decodeIfPresent(String.self, forKey: .name) name = try container.decodeIfPresent(String.self, forKey: .name)
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self) var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(uri, forKey: .uri) try container.encode(uri, forKey: .uri)
try container.encodeIfPresent(name, forKey: .name) try container.encodeIfPresent(name, forKey: .name)
try container.encodeIfPresent(_meta, forKey: ._meta)
} }
} }

View file

@ -7,7 +7,7 @@ import Foundation
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works /// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/sampling#how-sampling-works
public enum Sampling { public enum Sampling {
/// A message in the conversation history. /// A message in the conversation history.
public struct Message: Hashable, Codable, Sendable { public struct Message: Hashable, Sendable {
/// The message role /// The message role
public enum Role: String, Hashable, Codable, Sendable { public enum Role: String, Hashable, Codable, Sendable {
/// A user message /// A user message
@ -20,30 +20,34 @@ public enum Sampling {
public let role: Role public let role: Role
/// The message content /// The message content
public let content: Content public let content: Content
/// Optional metadata
public var _meta: Metadata?
/// Creates a message with the specified role and content /// Creates a message with the specified role and content
@available( @available(
*, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead" *, deprecated, message: "Use static factory methods .user(_:) or .assistant(_:) instead"
) )
public init(role: Role, content: Content) { public init(role: Role, content: Content, _meta: Metadata? = nil) {
self.role = role self.role = role
self.content = content self.content = content
self._meta = _meta
} }
/// Private initializer for convenience methods to avoid deprecation warnings /// Private initializer for convenience methods to avoid deprecation warnings
private init(_role role: Role, _content content: Content) { private init(_role role: Role, _content content: Content, _meta: Metadata? = nil) {
self.role = role self.role = role
self.content = content self.content = content
self._meta = _meta
} }
/// Creates a user message with the specified content /// Creates a user message with the specified content
public static func user(_ content: Content) -> Message { public static func user(_ content: Content, _meta: Metadata? = nil) -> Message {
return Message(_role: .user, _content: content) return Message(_role: .user, _content: content, _meta: _meta)
} }
/// Creates an assistant message with the specified content /// Creates an assistant message with the specified content
public static func assistant(_ content: Content) -> Message { public static func assistant(_ content: Content, _meta: Metadata? = nil) -> Message {
return Message(_role: .assistant, _content: content) return Message(_role: .assistant, _content: content, _meta: _meta)
} }
/// Content types for sampling messages /// Content types for sampling messages
@ -144,16 +148,25 @@ public enum Sampling {
case allServers case allServers
} }
/// Stop reason for sampling completion /// Stop reason for sampling completion.
public enum StopReason: String, Hashable, Codable, Sendable { ///
/// The spec defines this as an open string — any provider-specific value is valid.
/// The well-known values are exposed as static constants.
public struct StopReason: RawRepresentable, Hashable, Codable, Sendable,
ExpressibleByStringLiteral
{
public let rawValue: String
public init(rawValue: String) { self.rawValue = rawValue }
public init(stringLiteral value: String) { self.rawValue = value }
/// Natural end of turn /// Natural end of turn
case endTurn public static let endTurn = StopReason(rawValue: "endTurn")
/// Hit a stop sequence /// Hit a stop sequence
case stopSequence public static let stopSequence = StopReason(rawValue: "stopSequence")
/// Reached maximum tokens /// Reached maximum tokens
case maxTokens public static let maxTokens = StopReason(rawValue: "maxTokens")
/// Model wants to use a tool /// Model wants to use a tool
case toolUse public static let toolUse = StopReason(rawValue: "toolUse")
} }
/// Content representing a tool use request from the model /// Content representing a tool use request from the model
@ -220,6 +233,26 @@ public enum Sampling {
// MARK: - Codable // MARK: - Codable
extension Sampling.Message: Codable {
private enum CodingKeys: String, CodingKey {
case role, content, _meta
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
role = try container.decode(Role.self, forKey: .role)
content = try container.decode(Content.self, forKey: .content)
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(role, forKey: .role)
try container.encode(content, forKey: .content)
try container.encodeIfPresent(_meta, forKey: ._meta)
}
}
extension Sampling.Message.Content.ContentBlock: Codable { extension Sampling.Message.Content.ContentBlock: Codable {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType case type, text, data, mimeType
@ -243,13 +276,13 @@ extension Sampling.Message.Content.ContentBlock: Codable {
let data = try container.decode(String.self, forKey: .data) let data = try container.decode(String.self, forKey: .data)
let mimeType = try container.decode(String.self, forKey: .mimeType) let mimeType = try container.decode(String.self, forKey: .mimeType)
self = .audio(data: data, mimeType: mimeType) self = .audio(data: data, mimeType: mimeType)
case "toolUse": case "tool_use":
let id = try container.decode(String.self, forKey: .id) let id = try container.decode(String.self, forKey: .id)
let name = try container.decode(String.self, forKey: .name) let name = try container.decode(String.self, forKey: .name)
let input = try container.decode([String: Value].self, forKey: .input) let input = try container.decode([String: Value].self, forKey: .input)
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta) let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
self = .toolUse(Sampling.ToolUseContent(id: id, name: name, input: input, _meta: _meta)) self = .toolUse(Sampling.ToolUseContent(id: id, name: name, input: input, _meta: _meta))
case "toolResult": case "tool_result":
let toolUseId = try container.decode(String.self, forKey: .toolUseId) let toolUseId = try container.decode(String.self, forKey: .toolUseId)
let content = try container.decode([Sampling.ToolResultContent.ContentBlock].self, forKey: .content) let content = try container.decode([Sampling.ToolResultContent.ContentBlock].self, forKey: .content)
let structuredContent = try container.decodeIfPresent([String: Value].self, forKey: .structuredContent) let structuredContent = try container.decodeIfPresent([String: Value].self, forKey: .structuredContent)
@ -284,13 +317,13 @@ extension Sampling.Message.Content.ContentBlock: Codable {
try container.encode(data, forKey: .data) try container.encode(data, forKey: .data)
try container.encode(mimeType, forKey: .mimeType) try container.encode(mimeType, forKey: .mimeType)
case .toolUse(let toolUse): case .toolUse(let toolUse):
try container.encode("toolUse", forKey: .type) try container.encode("tool_use", forKey: .type)
try container.encode(toolUse.id, forKey: .id) try container.encode(toolUse.id, forKey: .id)
try container.encode(toolUse.name, forKey: .name) try container.encode(toolUse.name, forKey: .name)
try container.encode(toolUse.input, forKey: .input) try container.encode(toolUse.input, forKey: .input)
try container.encodeIfPresent(toolUse._meta, forKey: ._meta) try container.encodeIfPresent(toolUse._meta, forKey: ._meta)
case .toolResult(let toolResult): case .toolResult(let toolResult):
try container.encode("toolResult", forKey: .type) try container.encode("tool_result", forKey: .type)
try container.encode(toolResult.toolUseId, forKey: .toolUseId) try container.encode(toolResult.toolUseId, forKey: .toolUseId)
try container.encode(toolResult.content, forKey: .content) try container.encode(toolResult.content, forKey: .content)
try container.encodeIfPresent(toolResult.structuredContent, forKey: .structuredContent) try container.encodeIfPresent(toolResult.structuredContent, forKey: .structuredContent)
@ -372,7 +405,7 @@ extension Sampling.ToolResultContent.ContentBlock: Codable {
let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations) let annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta) let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
self = .resource(resource: resourceContent, annotations: annotations, _meta: _meta) self = .resource(resource: resourceContent, annotations: annotations, _meta: _meta)
case "resourceLink": case "resource_link":
let uri = try container.decode(String.self, forKey: .uri) let uri = try container.decode(String.self, forKey: .uri)
let name = try container.decode(String.self, forKey: .name) let name = try container.decode(String.self, forKey: .name)
let title = try container.decodeIfPresent(String.self, forKey: .title) let title = try container.decodeIfPresent(String.self, forKey: .title)
@ -410,7 +443,7 @@ extension Sampling.ToolResultContent.ContentBlock: Codable {
try container.encodeIfPresent(annotations, forKey: .annotations) try container.encodeIfPresent(annotations, forKey: .annotations)
try container.encodeIfPresent(_meta, forKey: ._meta) try container.encodeIfPresent(_meta, forKey: ._meta)
case .resourceLink(let uri, let name, let title, let description, let mimeType, let annotations): 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(uri, forKey: .uri)
try container.encode(name, forKey: .name) try container.encode(name, forKey: .name)
try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(title, forKey: .title)
@ -440,12 +473,28 @@ public enum CreateSamplingMessage: Method {
case none case none
} }
/// The tool choice mode /// The tool choice mode. If omitted, defaults to `.auto`.
public let mode: Mode public let mode: Mode?
public init(mode: Mode) { public init(mode: Mode? = .auto) {
self.mode = mode self.mode = mode
} }
private enum CodingKeys: String, CodingKey {
case mode
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
mode = try container.decodeIfPresent(Mode.self, forKey: .mode) ?? .auto
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if let mode, mode != .auto {
try container.encode(mode, forKey: .mode)
}
}
} }
public struct Parameters: Hashable, Codable, Sendable { public struct Parameters: Hashable, Codable, Sendable {
@ -469,6 +518,8 @@ public enum CreateSamplingMessage: Method {
public let tools: [Tool]? public let tools: [Tool]?
/// Tool choice configuration /// Tool choice configuration
public let toolChoice: ToolChoice? public let toolChoice: ToolChoice?
/// Provider-specific metadata to pass to the LLM
public let metadata: [String: Value]?
public init( public init(
messages: [Sampling.Message], messages: [Sampling.Message],
@ -480,7 +531,8 @@ public enum CreateSamplingMessage: Method {
stopSequences: [String]? = nil, stopSequences: [String]? = nil,
_meta: Metadata? = nil, _meta: Metadata? = nil,
tools: [Tool]? = nil, tools: [Tool]? = nil,
toolChoice: ToolChoice? = nil toolChoice: ToolChoice? = nil,
metadata: [String: Value]? = nil
) { ) {
self.messages = messages self.messages = messages
self.modelPreferences = modelPreferences self.modelPreferences = modelPreferences
@ -492,6 +544,7 @@ public enum CreateSamplingMessage: Method {
self._meta = _meta self._meta = _meta
self.tools = tools self.tools = tools
self.toolChoice = toolChoice self.toolChoice = toolChoice
self.metadata = metadata
} }
} }

View file

@ -7,38 +7,10 @@ import Foundation
// MARK: - Reference Types // MARK: - Reference Types
/// A reference to a prompt by name /// A reference to a prompt by name.
public struct PromptReference: Hashable, Codable, Sendable { ///
/// The prompt name /// This is a typealias for `Prompt.Reference` — the two types are equivalent.
public let name: String public typealias PromptReference = Prompt.Reference
public init(name: String) {
self.name = name
}
private enum CodingKeys: String, CodingKey {
case type, name
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode("ref/prompt", forKey: .type)
try container.encode(name, forKey: .name)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
guard type == "ref/prompt" else {
throw DecodingError.dataCorruptedError(
forKey: .type,
in: container,
debugDescription: "Expected ref/prompt type"
)
}
name = try container.decode(String.self, forKey: .name)
}
}
/// A reference to a resource by URI /// A reference to a resource by URI
public struct ResourceReference: Hashable, Codable, Sendable { public struct ResourceReference: Hashable, Codable, Sendable {
@ -153,9 +125,9 @@ public enum Complete: Method {
/// Context containing already-resolved arguments /// Context containing already-resolved arguments
public struct Context: Hashable, Codable, Sendable { public struct Context: Hashable, Codable, Sendable {
/// A mapping of already-resolved argument names to their values /// 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 self.arguments = arguments
} }
} }
@ -164,9 +136,28 @@ public enum Complete: Method {
public struct Result: Hashable, Codable, Sendable { public struct Result: Hashable, Codable, Sendable {
/// The completion result /// The completion result
public let completion: Completion 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.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 /// Completion result containing suggested values

View file

@ -48,8 +48,8 @@ public struct Prompt: Hashable, Codable, Sendable {
try container.encodeIfPresent(title, forKey: .title) try container.encodeIfPresent(title, forKey: .title)
try container.encodeIfPresent(description, forKey: .description) try container.encodeIfPresent(description, forKey: .description)
try container.encodeIfPresent(arguments, forKey: .arguments) try container.encodeIfPresent(arguments, forKey: .arguments)
try container.encodeIfPresent(icons, forKey: . icons) try container.encodeIfPresent(icons, forKey: .icons)
try container.encodeIfPresent(_meta, forKey: . _meta) try container.encodeIfPresent(_meta, forKey: ._meta)
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -58,8 +58,8 @@ public struct Prompt: Hashable, Codable, Sendable {
title = try container.decodeIfPresent(String.self, forKey: .title) title = try container.decodeIfPresent(String.self, forKey: .title)
description = try container.decodeIfPresent(String.self, forKey: .description) description = try container.decodeIfPresent(String.self, forKey: .description)
arguments = try container.decodeIfPresent([Argument].self, forKey: .arguments) arguments = try container.decodeIfPresent([Argument].self, forKey: .arguments)
icons = try container.decodeIfPresent([Icon].self, forKey: . icons) icons = try container.decodeIfPresent([Icon].self, forKey: .icons)
_meta = try container.decodeIfPresent(Metadata.self, forKey: . _meta) _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
} }
/// An argument for a prompt /// An argument for a prompt
@ -136,6 +136,8 @@ public struct Prompt: Hashable, Codable, Sendable {
case audio(data: String, mimeType: String) case audio(data: String, mimeType: String)
/// Embedded resource content (EmbeddedResource from spec) /// Embedded resource content (EmbeddedResource from spec)
case resource(resource: Resource.Content, annotations: Resource.Annotations? = nil, _meta: Metadata? = nil) 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 { extension Prompt.Message.Content: Codable {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case type, text, data, mimeType, resource, annotations, _meta case type, text, data, mimeType, resource, annotations, _meta
case uri, name, title, description
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -198,6 +201,14 @@ extension Prompt.Message.Content: Codable {
try container.encode(resourceContent, forKey: .resource) try container.encode(resourceContent, forKey: .resource)
try container.encodeIfPresent(annotations, forKey: .annotations) try container.encodeIfPresent(annotations, forKey: .annotations)
try container.encodeIfPresent(_meta, forKey: ._meta) 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 annotations = try container.decodeIfPresent(Resource.Annotations.self, forKey: .annotations)
let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta) let _meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta)
self = .resource(resource: resourceContent, annotations: annotations, _meta: _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: default:
throw DecodingError.dataCorruptedError( throw DecodingError.dataCorruptedError(
forKey: .type, forKey: .type,
@ -250,7 +269,7 @@ extension Prompt.Message.Content: ExpressibleByStringInterpolation {
// MARK: - // MARK: -
/// To retrieve available prompts, clients send a `prompts/list` request. /// 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 enum ListPrompts: Method {
public static let name: String = "prompts/list" public static let name: String = "prompts/list"
@ -267,9 +286,9 @@ public enum ListPrompts: Method {
} }
public struct Result: Hashable, Codable, Sendable { public struct Result: Hashable, Codable, Sendable {
let prompts: [Prompt] public let prompts: [Prompt]
let nextCursor: String? public let nextCursor: String?
var _meta: Metadata? public var _meta: Metadata?
public init( public init(
prompts: [Prompt], prompts: [Prompt],
@ -303,15 +322,15 @@ public enum ListPrompts: Method {
/// To retrieve a specific prompt, clients send a `prompts/get` request. /// To retrieve a specific prompt, clients send a `prompts/get` request.
/// Arguments may be auto-completed through the completion API. /// 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 enum GetPrompt: Method {
public static let name: String = "prompts/get" public static let name: String = "prompts/get"
public struct Parameters: Hashable, Codable, Sendable { public struct Parameters: Hashable, Codable, Sendable {
public let name: String 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.name = name
self.arguments = arguments 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. /// 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 struct PromptListChangedNotification: Notification {
public static let name: String = "notifications/prompts/list_changed" public static let name: String = "notifications/prompts/list_changed"
} }

View file

@ -18,8 +18,10 @@ public struct Resource: Hashable, Codable, Sendable {
public var description: String? public var description: String?
/// The resource MIME type /// The resource MIME type
public var mimeType: String? public var mimeType: String?
/// The resource metadata /// Raw content size in bytes
public var metadata: [String: String]? 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 /// Optional set of sized icons that the client can display in a user interface
public var icons: [Icon]? public var icons: [Icon]?
/// Metadata fields for the resource (see spec for _meta usage) /// Metadata fields for the resource (see spec for _meta usage)
@ -31,7 +33,8 @@ public struct Resource: Hashable, Codable, Sendable {
title: String? = nil, title: String? = nil,
description: String? = nil, description: String? = nil,
mimeType: String? = nil, mimeType: String? = nil,
metadata: [String: String]? = nil, size: Int? = nil,
annotations: Resource.Annotations? = nil,
icons: [Icon]? = nil, icons: [Icon]? = nil,
_meta: Metadata? = nil _meta: Metadata? = nil
) { ) {
@ -40,7 +43,8 @@ public struct Resource: Hashable, Codable, Sendable {
self.uri = uri self.uri = uri
self.description = description self.description = description
self.mimeType = mimeType self.mimeType = mimeType
self.metadata = metadata self.size = size
self.annotations = annotations
self.icons = icons self.icons = icons
self._meta = _meta self._meta = _meta
} }
@ -51,7 +55,8 @@ public struct Resource: Hashable, Codable, Sendable {
case title case title
case description case description
case mimeType case mimeType
case metadata case size
case annotations
case icons case icons
case _meta case _meta
} }
@ -63,7 +68,8 @@ public struct Resource: Hashable, Codable, Sendable {
title = try container.decodeIfPresent(String.self, forKey: .title) title = try container.decodeIfPresent(String.self, forKey: .title)
description = try container.decodeIfPresent(String.self, forKey: .description) description = try container.decodeIfPresent(String.self, forKey: .description)
mimeType = try container.decodeIfPresent(String.self, forKey: .mimeType) 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) icons = try container.decodeIfPresent([Icon].self, forKey: .icons)
_meta = try container.decodeIfPresent(Metadata.self, forKey: ._meta) _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(title, forKey: .title)
try container.encodeIfPresent(description, forKey: .description) try container.encodeIfPresent(description, forKey: .description)
try container.encodeIfPresent(mimeType, forKey: .mimeType) 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(icons, forKey: .icons)
try container.encodeIfPresent(_meta, forKey: ._meta) try container.encodeIfPresent(_meta, forKey: ._meta)
} }
@ -181,8 +188,12 @@ public struct Resource: Hashable, Codable, Sendable {
public var description: String? public var description: String?
/// The resource MIME type /// The resource MIME type
public var mimeType: String? 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 /// Optional set of sized icons that the client can display in a user interface
public var icons: [Icon]? public var icons: [Icon]?
/// Metadata fields for the template (see spec for _meta usage)
public var _meta: Metadata?
public init( public init(
uriTemplate: String, uriTemplate: String,
@ -190,14 +201,18 @@ public struct Resource: Hashable, Codable, Sendable {
title: String? = nil, title: String? = nil,
description: String? = nil, description: String? = nil,
mimeType: String? = nil, mimeType: String? = nil,
icons: [Icon]? = nil annotations: Resource.Annotations? = nil,
icons: [Icon]? = nil,
_meta: Metadata? = nil
) { ) {
self.uriTemplate = uriTemplate self.uriTemplate = uriTemplate
self.name = name self.name = name
self.title = title self.title = title
self.description = description self.description = description
self.mimeType = mimeType self.mimeType = mimeType
self.annotations = annotations
self.icons = icons self.icons = icons
self._meta = _meta
} }
} }
@ -233,7 +248,7 @@ public struct Resource: Hashable, Codable, Sendable {
// MARK: - // MARK: -
/// To discover available resources, clients send a `resources/list` request. /// 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 enum ListResources: Method {
public static let name: String = "resources/list" 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: /// 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 enum ReadResource: Method {
public static let name: String = "resources/read" 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. /// 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 enum ListResourceTemplates: Method {
public static let name: String = "resources/templates/list" 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. /// 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 struct ResourceListChangedNotification: Notification {
public static let name: String = "notifications/resources/list_changed" 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. /// 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 enum ResourceSubscribe: Method {
public static let name: String = "resources/subscribe" 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. /// 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 struct ResourceUpdatedNotification: Notification {
public static let name: String = "notifications/resources/updated" public static let name: String = "notifications/resources/updated"

View file

@ -34,11 +34,27 @@ public actor Server {
public let title: String? public let title: String?
/// The server version /// The server version
public let version: String 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.name = name
self.title = title self.title = title
self.version = version self.version = version
self.description = description
self.websiteUrl = websiteUrl
self.icons = icons
} }
} }
@ -85,11 +101,6 @@ public actor Server {
public init() {} public init() {}
} }
/// Sampling capabilities
public struct Sampling: Hashable, Codable, Sendable {
public init() {}
}
/// Completions capabilities /// Completions capabilities
public struct Completions: Hashable, Codable, Sendable { public struct Completions: Hashable, Codable, Sendable {
public init() {} public init() {}
@ -103,8 +114,6 @@ public actor Server {
public var prompts: Prompts? public var prompts: Prompts?
/// Resources capabilities /// Resources capabilities
public var resources: Resources? public var resources: Resources?
/// Sampling capabilities
public var sampling: Sampling?
/// Tools capabilities /// Tools capabilities
public var tools: Tools? public var tools: Tools?
@ -113,14 +122,12 @@ public actor Server {
logging: Logging? = nil, logging: Logging? = nil,
prompts: Prompts? = nil, prompts: Prompts? = nil,
resources: Resources? = nil, resources: Resources? = nil,
sampling: Sampling? = nil,
tools: Tools? = nil tools: Tools? = nil
) { ) {
self.completions = completions self.completions = completions
self.logging = logging self.logging = logging
self.prompts = prompts self.prompts = prompts
self.resources = resources self.resources = resources
self.sampling = sampling
self.tools = tools self.tools = tools
} }
} }
@ -161,47 +168,6 @@ public actor Server {
/// Pending request tasks (for cancellation support) /// Pending request tasks (for cancellation support)
private var pendingRequestTasks: [ID: Task<Response<AnyMethod>, Error>] = [:] 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 /// Pending requests sent to the client, awaiting responses
private var pendingRequests: [ID: AnyPendingRequest] = [:] private var pendingRequests: [ID: AnyPendingRequest] = [:]
@ -330,6 +296,16 @@ public actor Server {
await task?.value 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 // MARK: - Registration
/// Register a method handler /// Register a method handler
@ -523,8 +499,8 @@ public actor Server {
/// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/elicitation /// - SeeAlso: https://modelcontextprotocol.io/docs/concepts/elicitation
public func requestElicitation( public func requestElicitation(
message: String, message: String,
requestedSchema: Elicitation.RequestSchema,
mode: Elicitation.Mode? = nil, mode: Elicitation.Mode? = nil,
requestedSchema: Elicitation.RequestSchema? = nil,
_meta: Metadata? = nil _meta: Metadata? = nil
) async throws -> CreateElicitation.Result { ) async throws -> CreateElicitation.Result {
guard connection != nil else { guard connection != nil else {
@ -774,26 +750,30 @@ public actor Server {
return response return response
} }
// Create a task to handle the request with cancellation support // Create a task to handle the request with cancellation support.
let handlerTask = Task<Response<AnyMethod>, Error> { // Set currentRequestID as a task local so handlers can identify the active request.
do { var handlerTask: Task<Response<AnyMethod>, Error>!
// Check if task was cancelled before starting Server.$currentRequestID.withValue(request.id) {
try Task.checkCancellation() handlerTask = Task<Response<AnyMethod>, Error> {
do {
// Check if task was cancelled before starting
try Task.checkCancellation()
// Handle request and get response // Handle request and get response
let response = try await handler(request) let response = try await handler(request)
return response return response
} catch is CancellationError { } catch is CancellationError {
// Request was cancelled, don't send a response per MCP spec // Request was cancelled, don't send a response per MCP spec
await logger?.debug( await logger?.debug(
"Request cancelled", "Request cancelled",
metadata: ["id": "\(request.id)", "method": "\(request.method)"] metadata: ["id": "\(request.id)", "method": "\(request.method)"]
) )
throw CancellationError() throw CancellationError()
} catch { } catch {
let mcpError = let mcpError =
error as? MCPError ?? MCPError.internalError(error.localizedDescription) error as? MCPError ?? MCPError.internalError(error.localizedDescription)
return AnyMethod.response(id: request.id, error: mcpError) return AnyMethod.response(id: request.id, error: mcpError)
}
} }
} }
@ -969,11 +949,19 @@ public actor Server {
await self.logger?.debug( await self.logger?.debug(
"Received cancellation notification", "Received cancellation notification",
metadata: [ metadata: [
"requestId": "\(requestId)", "requestId": requestId.map { "\($0)" } ?? "none",
"reason": reason.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 // Cancel the pending request task if it exists and remove from tracking
if let task = await self.removePendingRequest(id: requestId) { if let task = await self.removePendingRequest(id: requestId) {
task.cancel() task.cancel()

Some files were not shown because too many files have changed in this diff Show more