Allow params field to be omitted in requests for methods that don't require them (#17)

* Allow `params` field to be omitted in requests for methods with empty parameters

* Add ping request to roundtrip test

* Generalize decoding of non-required parameters
This commit is contained in:
Mattt 2025-03-17 05:26:47 -07:00 • committed by GitHub
commit 6f1f65a392
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 206 additions and 25 deletions

View file

@ -2,7 +2,13 @@ import Foundation
private let jsonrpc = "2.0"
public struct Empty: Hashable, Codable, Sendable {}
public protocol NotRequired {
init()
}
public struct Empty: NotRequired, Hashable, Codable, Sendable {
public init() {}
}
// MARK: -
@ -78,14 +84,11 @@ public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
try container.encode(jsonrpc, forKey: .jsonrpc)
try container.encode(id, forKey: .id)
try container.encode(method, forKey: .method)
if M.Parameters.self != Empty.self {
try container.encode(params, forKey: .params)
} else {
// Encode empty object for Empty parameters
try container.encode(Empty(), forKey: .params)
}
try container.encode(params, forKey: .params)
}
}
extension Request {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let version = try container.decode(String.self, forKey: .jsonrpc)
@ -95,15 +98,11 @@ public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
}
id = try container.decode(ID.self, forKey: .id)
method = try container.decode(String.self, forKey: .method)
if M.Parameters.self == Empty.self {
if (try? container.decodeNil(forKey: .params)) != nil {
params = Empty() as! M.Parameters
} else if (try? container.decode(Empty.self, forKey: .params)) != nil {
params = Empty() as! M.Parameters
} else {
// If params field is missing, use Empty
params = Empty() as! M.Parameters
}
if M.Parameters.self is NotRequired.Type {
params =
(try container.decodeIfPresent(M.Parameters.self, forKey: .params)
?? (M.Parameters.self as! NotRequired.Type).init() as! M.Parameters)
} else {
params = try container.decode(M.Parameters.self, forKey: .params)
}

View file

@ -305,7 +305,12 @@ public actor Client {
-> (prompts: [Prompt], nextCursor: String?)
{
_ = try checkCapability(\.prompts, "Prompts")
let request = ListPrompts.request(.init(cursor: cursor))
let request: Request<ListPrompts>
if let cursor = cursor {
request = ListPrompts.request(.init(cursor: cursor))
} else {
request = ListPrompts.request(.init())
}
let result = try await send(request)
return (prompts: result.prompts, nextCursor: result.nextCursor)
}
@ -323,7 +328,12 @@ public actor Client {
resources: [Resource], nextCursor: String?
) {
_ = try checkCapability(\.resources, "Resources")
let request = ListResources.request(.init(cursor: cursor))
let request: Request<ListResources>
if let cursor = cursor {
request = ListResources.request(.init(cursor: cursor))
} else {
request = ListResources.request(.init())
}
let result = try await send(request)
return (resources: result.resources, nextCursor: result.nextCursor)
}
@ -338,7 +348,12 @@ public actor Client {
public func listTools(cursor: String? = nil) async throws -> [Tool] {
_ = try checkCapability(\.tools, "Tools")
let request = ListTools.request(.init(cursor: cursor))
let request: Request<ListTools>
if let cursor = cursor {
request = ListTools.request(.init(cursor: cursor))
} else {
request = ListTools.request(.init())
}
let result = try await send(request)
return result.tools
}

View file

@ -153,10 +153,14 @@ public struct Prompt: Hashable, Codable, Sendable {
public enum ListPrompts: Method {
public static let name: String = "prompts/list"
public struct Parameters: Hashable, Codable, Sendable {
public struct Parameters: NotRequired, Hashable, Codable, Sendable {
public let cursor: String?
public init() {
self.cursor = nil
}
public init(cursor: String? = nil) {
public init(cursor: String) {
self.cursor = cursor
}
}

View file

@ -99,10 +99,14 @@ public struct Resource: Hashable, Codable, Sendable {
public enum ListResources: Method {
public static let name: String = "resources/list"
public struct Parameters: Hashable, Codable, Sendable {
public struct Parameters: NotRequired, Hashable, Codable, Sendable {
public let cursor: String?
public init(cursor: String? = nil) {
public init() {
self.cursor = nil
}
public init(cursor: String) {
self.cursor = cursor
}
}

View file

@ -118,10 +118,14 @@ public struct Tool: Hashable, Codable, Sendable {
public enum ListTools: Method {
public static let name = "tools/list"
public struct Parameters: Hashable, Codable, Sendable {
public struct Parameters: NotRequired, Hashable, Codable, Sendable {
public let cursor: String?
public init() {
self.cursor = nil
}
public init(cursor: String? = nil) {
public init(cursor: String) {
self.cursor = cursor
}
}