Add support for JSON-RPC batching (#67)

* Update StdioTransport documentation comments to indicate support for JSON-RPC batches

* Add server support for JSON-RPC batching

* Add client support for JSON-RPC batching

* Fix bug that caused error messages to be wrapped unnecessarily
This commit is contained in:
Mattt 2025-04-10 12:13:41 -07:00 • committed by GitHub
commit 82f4fd29b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -226,4 +226,141 @@ struct ClientTests {
// Disconnect client
await client.disconnect()
}
@Test("Batch request - success")
func testBatchRequestSuccess() async throws {
let transport = MockTransport()
let client = Client(name: "TestClient", version: "1.0")
try await client.connect(transport: transport)
try await Task.sleep(for: .milliseconds(10)) // Allow connection tasks
let request1 = Ping.request()
let request2 = Ping.request()
var resultTask1: Task<Ping.Result, Swift.Error>?
var resultTask2: Task<Ping.Result, Swift.Error>?
try await client.withBatch { batch in
resultTask1 = try await batch.addRequest(request1)
resultTask2 = try await batch.addRequest(request2)
}
// Check if one batch message was sent
let sentMessages = await transport.sentMessages
#expect(sentMessages.count == 1)
guard let batchData = sentMessages.first?.data(using: .utf8) else {
#expect(Bool(false), "Failed to get batch data")
return
}
// Verify the sent batch contains the two requests
let decoder = JSONDecoder()
let sentRequests = try decoder.decode([AnyRequest].self, from: batchData)
#expect(sentRequests.count == 2)
#expect(sentRequests.first?.id == request1.id)
#expect(sentRequests.first?.method == Ping.name)
#expect(sentRequests.last?.id == request2.id)
#expect(sentRequests.last?.method == Ping.name)
// Prepare batch response
let response1 = Response<Ping>(id: request1.id, result: .init())
let response2 = Response<Ping>(id: request2.id, result: .init())
let anyResponse1 = try AnyResponse(response1)
let anyResponse2 = try AnyResponse(response2)
// Queue the batch response
try await transport.queue(batch: [anyResponse1, anyResponse2])
// Wait for results and verify
guard let task1 = resultTask1, let task2 = resultTask2 else {
#expect(Bool(false), "Result tasks not created")
return
}
_ = try await task1.value // Should succeed
_ = try await task2.value // Should succeed
#expect(Bool(true)) // Reaching here means success
await client.disconnect()
}
@Test("Batch request - mixed success/error")
func testBatchRequestMixed() async throws {
let transport = MockTransport()
let client = Client(name: "TestClient", version: "1.0")
try await client.connect(transport: transport)
try await Task.sleep(for: .milliseconds(10))
let request1 = Ping.request() // Success
let request2 = Ping.request() // Error
var resultTasks: [Task<Ping.Result, Swift.Error>] = []
try await client.withBatch { batch in
resultTasks.append(try await batch.addRequest(request1))
resultTasks.append(try await batch.addRequest(request2))
}
// Check if one batch message was sent
#expect(await transport.sentMessages.count == 1)
// Prepare batch response (success for 1, error for 2)
let response1 = Response<Ping>(id: request1.id, result: .init())
let error = MCPError.internalError("Simulated batch error")
let response2 = Response<Ping>(id: request2.id, error: error)
let anyResponse1 = try AnyResponse(response1)
let anyResponse2 = try AnyResponse(response2)
// Queue the batch response
try await transport.queue(batch: [anyResponse1, anyResponse2])
// Wait for results and verify
#expect(resultTasks.count == 2)
guard resultTasks.count == 2 else {
#expect(Bool(false), "Expected 2 result tasks")
return
}
let task1 = resultTasks[0]
let task2 = resultTasks[1]
_ = try await task1.value // Task 1 should succeed
do {
_ = try await task2.value // Task 2 should fail
#expect(Bool(false), "Task 2 should have thrown an error")
} catch let mcpError as MCPError {
if case .internalError(let message) = mcpError {
#expect(message == "Simulated batch error")
} else {
#expect(Bool(false), "Expected internalError, got \(mcpError)")
}
} catch {
#expect(Bool(false), "Expected MCPError, got \(error)")
}
await client.disconnect()
}
@Test("Batch request - empty")
func testBatchRequestEmpty() async throws {
let transport = MockTransport()
let client = Client(name: "TestClient", version: "1.0")
try await client.connect(transport: transport)
try await Task.sleep(for: .milliseconds(10))
// Call withBatch but don't add any requests
try await client.withBatch { _ in
// No requests added
}
// Check that no messages were sent
#expect(await transport.sentMessages.isEmpty)
await client.disconnect()
}
}