Skip to content

Commit 6f1f65a

Browse files
authored
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
1 parent 3e628db commit 6f1f65a

File tree

10 files changed

+206
-25
lines changed

10 files changed

+206
-25
lines changed

Sources/MCP/Base/Messages.swift

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import Foundation
22

33
private let jsonrpc = "2.0"
44

5-
public struct Empty: Hashable, Codable, Sendable {}
5+
public protocol NotRequired {
6+
init()
7+
}
8+
9+
public struct Empty: NotRequired, Hashable, Codable, Sendable {
10+
public init() {}
11+
}
612

713
// MARK: -
814

@@ -78,14 +84,11 @@ public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
7884
try container.encode(jsonrpc, forKey: .jsonrpc)
7985
try container.encode(id, forKey: .id)
8086
try container.encode(method, forKey: .method)
81-
if M.Parameters.self != Empty.self {
82-
try container.encode(params, forKey: .params)
83-
} else {
84-
// Encode empty object for Empty parameters
85-
try container.encode(Empty(), forKey: .params)
86-
}
87+
try container.encode(params, forKey: .params)
8788
}
89+
}
8890

91+
extension Request {
8992
public init(from decoder: Decoder) throws {
9093
let container = try decoder.container(keyedBy: CodingKeys.self)
9194
let version = try container.decode(String.self, forKey: .jsonrpc)
@@ -95,15 +98,11 @@ public struct Request<M: Method>: Hashable, Identifiable, Codable, Sendable {
9598
}
9699
id = try container.decode(ID.self, forKey: .id)
97100
method = try container.decode(String.self, forKey: .method)
98-
if M.Parameters.self == Empty.self {
99-
if (try? container.decodeNil(forKey: .params)) != nil {
100-
params = Empty() as! M.Parameters
101-
} else if (try? container.decode(Empty.self, forKey: .params)) != nil {
102-
params = Empty() as! M.Parameters
103-
} else {
104-
// If params field is missing, use Empty
105-
params = Empty() as! M.Parameters
106-
}
101+
102+
if M.Parameters.self is NotRequired.Type {
103+
params =
104+
(try container.decodeIfPresent(M.Parameters.self, forKey: .params)
105+
?? (M.Parameters.self as! NotRequired.Type).init() as! M.Parameters)
107106
} else {
108107
params = try container.decode(M.Parameters.self, forKey: .params)
109108
}

Sources/MCP/Client/Client.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,12 @@ public actor Client {
305305
-> (prompts: [Prompt], nextCursor: String?)
306306
{
307307
_ = try checkCapability(\.prompts, "Prompts")
308-
let request = ListPrompts.request(.init(cursor: cursor))
308+
let request: Request<ListPrompts>
309+
if let cursor = cursor {
310+
request = ListPrompts.request(.init(cursor: cursor))
311+
} else {
312+
request = ListPrompts.request(.init())
313+
}
309314
let result = try await send(request)
310315
return (prompts: result.prompts, nextCursor: result.nextCursor)
311316
}
@@ -323,7 +328,12 @@ public actor Client {
323328
resources: [Resource], nextCursor: String?
324329
) {
325330
_ = try checkCapability(\.resources, "Resources")
326-
let request = ListResources.request(.init(cursor: cursor))
331+
let request: Request<ListResources>
332+
if let cursor = cursor {
333+
request = ListResources.request(.init(cursor: cursor))
334+
} else {
335+
request = ListResources.request(.init())
336+
}
327337
let result = try await send(request)
328338
return (resources: result.resources, nextCursor: result.nextCursor)
329339
}
@@ -338,7 +348,12 @@ public actor Client {
338348

339349
public func listTools(cursor: String? = nil) async throws -> [Tool] {
340350
_ = try checkCapability(\.tools, "Tools")
341-
let request = ListTools.request(.init(cursor: cursor))
351+
let request: Request<ListTools>
352+
if let cursor = cursor {
353+
request = ListTools.request(.init(cursor: cursor))
354+
} else {
355+
request = ListTools.request(.init())
356+
}
342357
let result = try await send(request)
343358
return result.tools
344359
}

Sources/MCP/Server/Prompts.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,14 @@ public struct Prompt: Hashable, Codable, Sendable {
153153
public enum ListPrompts: Method {
154154
public static let name: String = "prompts/list"
155155

156-
public struct Parameters: Hashable, Codable, Sendable {
156+
public struct Parameters: NotRequired, Hashable, Codable, Sendable {
157157
public let cursor: String?
158+
159+
public init() {
160+
self.cursor = nil
161+
}
158162

159-
public init(cursor: String? = nil) {
163+
public init(cursor: String) {
160164
self.cursor = cursor
161165
}
162166
}

Sources/MCP/Server/Resources.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,14 @@ public struct Resource: Hashable, Codable, Sendable {
9999
public enum ListResources: Method {
100100
public static let name: String = "resources/list"
101101

102-
public struct Parameters: Hashable, Codable, Sendable {
102+
public struct Parameters: NotRequired, Hashable, Codable, Sendable {
103103
public let cursor: String?
104104

105-
public init(cursor: String? = nil) {
105+
public init() {
106+
self.cursor = nil
107+
}
108+
109+
public init(cursor: String) {
106110
self.cursor = cursor
107111
}
108112
}

Sources/MCP/Server/Tools.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,14 @@ public struct Tool: Hashable, Codable, Sendable {
118118
public enum ListTools: Method {
119119
public static let name = "tools/list"
120120

121-
public struct Parameters: Hashable, Codable, Sendable {
121+
public struct Parameters: NotRequired, Hashable, Codable, Sendable {
122122
public let cursor: String?
123+
124+
public init() {
125+
self.cursor = nil
126+
}
123127

124-
public init(cursor: String? = nil) {
128+
public init(cursor: String) {
125129
self.cursor = cursor
126130
}
127131
}

Tests/MCPTests/PromptTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,36 @@ struct PromptTests {
142142
let emptyParams = ListPrompts.Parameters()
143143
#expect(emptyParams.cursor == nil)
144144
}
145+
146+
@Test("ListPrompts request decoding with omitted params")
147+
func testListPromptsRequestDecodingWithOmittedParams() throws {
148+
// Test decoding when params field is omitted
149+
let jsonString = """
150+
{"jsonrpc":"2.0","id":"test-id","method":"prompts/list"}
151+
"""
152+
let data = jsonString.data(using: .utf8)!
153+
154+
let decoder = JSONDecoder()
155+
let decoded = try decoder.decode(Request<ListPrompts>.self, from: data)
156+
157+
#expect(decoded.id == "test-id")
158+
#expect(decoded.method == ListPrompts.name)
159+
}
160+
161+
@Test("ListPrompts request decoding with null params")
162+
func testListPromptsRequestDecodingWithNullParams() throws {
163+
// Test decoding when params field is null
164+
let jsonString = """
165+
{"jsonrpc":"2.0","id":"test-id","method":"prompts/list","params":null}
166+
"""
167+
let data = jsonString.data(using: .utf8)!
168+
169+
let decoder = JSONDecoder()
170+
let decoded = try decoder.decode(Request<ListPrompts>.self, from: data)
171+
172+
#expect(decoded.id == "test-id")
173+
#expect(decoded.method == ListPrompts.name)
174+
}
145175

146176
@Test("ListPrompts result validation")
147177
func testListPromptsResult() throws {

Tests/MCPTests/RequestTests.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,49 @@ struct RequestTests {
7676
#expect(decoded.id == "test-id")
7777
#expect(decoded.method == EmptyMethod.name)
7878
}
79+
80+
@Test("NotRequired parameters request decoding - with params")
81+
func testNotRequiredParametersRequestDecodingWithParams() throws {
82+
// Test decoding when params field is present
83+
let jsonString = """
84+
{"jsonrpc":"2.0","id":"test-id","method":"ping","params":{}}
85+
"""
86+
let data = jsonString.data(using: .utf8)!
87+
88+
let decoder = JSONDecoder()
89+
let decoded = try decoder.decode(Request<Ping>.self, from: data)
90+
91+
#expect(decoded.id == "test-id")
92+
#expect(decoded.method == Ping.name)
93+
}
94+
95+
@Test("NotRequired parameters request decoding - without params")
96+
func testNotRequiredParametersRequestDecodingWithoutParams() throws {
97+
// Test decoding when params field is missing
98+
let jsonString = """
99+
{"jsonrpc":"2.0","id":"test-id","method":"ping"}
100+
"""
101+
let data = jsonString.data(using: .utf8)!
102+
103+
let decoder = JSONDecoder()
104+
let decoded = try decoder.decode(Request<Ping>.self, from: data)
105+
106+
#expect(decoded.id == "test-id")
107+
#expect(decoded.method == Ping.name)
108+
}
109+
110+
@Test("NotRequired parameters request decoding - with null params")
111+
func testNotRequiredParametersRequestDecodingWithNullParams() throws {
112+
// Test decoding when params field is null
113+
let jsonString = """
114+
{"jsonrpc":"2.0","id":"test-id","method":"ping","params":null}
115+
"""
116+
let data = jsonString.data(using: .utf8)!
117+
118+
let decoder = JSONDecoder()
119+
let decoded = try decoder.decode(Request<Ping>.self, from: data)
120+
121+
#expect(decoded.id == "test-id")
122+
#expect(decoded.method == Ping.name)
123+
}
79124
}

Tests/MCPTests/ResourceTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,36 @@ struct ResourceTests {
8686
let emptyParams = ListResources.Parameters()
8787
#expect(emptyParams.cursor == nil)
8888
}
89+
90+
@Test("ListResources request decoding with omitted params")
91+
func testListResourcesRequestDecodingWithOmittedParams() throws {
92+
// Test decoding when params field is omitted
93+
let jsonString = """
94+
{"jsonrpc":"2.0","id":"test-id","method":"resources/list"}
95+
"""
96+
let data = jsonString.data(using: .utf8)!
97+
98+
let decoder = JSONDecoder()
99+
let decoded = try decoder.decode(Request<ListResources>.self, from: data)
100+
101+
#expect(decoded.id == "test-id")
102+
#expect(decoded.method == ListResources.name)
103+
}
104+
105+
@Test("ListResources request decoding with null params")
106+
func testListResourcesRequestDecodingWithNullParams() throws {
107+
// Test decoding when params field is null
108+
let jsonString = """
109+
{"jsonrpc":"2.0","id":"test-id","method":"resources/list","params":null}
110+
"""
111+
let data = jsonString.data(using: .utf8)!
112+
113+
let decoder = JSONDecoder()
114+
let decoded = try decoder.decode(Request<ListResources>.self, from: data)
115+
116+
#expect(decoded.id == "test-id")
117+
#expect(decoded.method == ListResources.name)
118+
}
89119

90120
@Test("ListResources result validation")
91121
func testListResourcesResult() throws {

Tests/MCPTests/RoundtripTests.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,26 @@ struct RoundtripTests {
118118
group.cancelAll()
119119
}
120120

121+
// Test ping
122+
let pingTask = Task {
123+
try await client.ping()
124+
// Ping doesn't return anything, so just getting here without throwing is success
125+
#expect(true) // Test passed if we reach this point
126+
}
127+
128+
try await withThrowingTaskGroup(of: Void.self) { group in
129+
group.addTask {
130+
try await Task.sleep(for: .seconds(1))
131+
pingTask.cancel()
132+
throw CancellationError()
133+
}
134+
group.addTask {
135+
try await pingTask.value
136+
}
137+
try await group.next()
138+
group.cancelAll()
139+
}
140+
121141
let listToolsTask = Task {
122142
let result = try await client.listTools()
123143
#expect(result.count == 1)

Tests/MCPTests/ToolTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,36 @@ struct ToolTests {
111111
let emptyParams = ListTools.Parameters()
112112
#expect(emptyParams.cursor == nil)
113113
}
114+
115+
@Test("ListTools request decoding with omitted params")
116+
func testListToolsRequestDecodingWithOmittedParams() throws {
117+
// Test decoding when params field is omitted
118+
let jsonString = """
119+
{"jsonrpc":"2.0","id":"test-id","method":"tools/list"}
120+
"""
121+
let data = jsonString.data(using: .utf8)!
122+
123+
let decoder = JSONDecoder()
124+
let decoded = try decoder.decode(Request<ListTools>.self, from: data)
125+
126+
#expect(decoded.id == "test-id")
127+
#expect(decoded.method == ListTools.name)
128+
}
129+
130+
@Test("ListTools request decoding with null params")
131+
func testListToolsRequestDecodingWithNullParams() throws {
132+
// Test decoding when params field is null
133+
let jsonString = """
134+
{"jsonrpc":"2.0","id":"test-id","method":"tools/list","params":null}
135+
"""
136+
let data = jsonString.data(using: .utf8)!
137+
138+
let decoder = JSONDecoder()
139+
let decoded = try decoder.decode(Request<ListTools>.self, from: data)
140+
141+
#expect(decoded.id == "test-id")
142+
#expect(decoded.method == ListTools.name)
143+
}
114144

115145
@Test("ListTools result validation")
116146
func testListToolsResult() throws {

0 commit comments

Comments
 (0)