feat: 2025-11-25 review fixes (#201)
* fix: review fixes of the latest changes * fix: updated unit tests * chore: more conformance compliance improvements * chore: reverted protocol changes * chore: more compliance improvements * chore: more conformance compliance improvements * chore: improvements to the backcompat and updated to Tool unit tests * chore: ci update
This commit is contained in:
parent
6112a3995a
commit
973b46b8f7
35 changed files with 692 additions and 507 deletions
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
Sources/MCP/Base/PendingRequest.swift
Normal file
43
Sources/MCP/Base/PendingRequest.swift
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Reference in a new issue