* Allow decoding Decodable type directly from Value Summary: I noticed we are not converting value associated with response to the strong types requests recorded This change will help to decode the value type later once we support the rest of plumbing (next commit) * Store and pass original request when response arrives Summary: Looks like there were multiple issues with parsing response: 1. It appears that we would mistakenly pass response both for response and request arguments in the private func handleResponse(_ response: Response<AnyMethod>, for request: Any) 2. Later I couldn't figure out how to win against swift type system, so ended up creating a type-erased request to store in the pendingRequests instead of original type under Any (that wouldn't allow to downcast original type later) Test Plan: 1. Standup iMCP client from https://github.com/loopwork-ai/iMCP 2. Create a dummy project setting up MCP client against "/private/var/folders/26/8gncz37x7slbfrr95d9jcr400000gn/T/AppTranslocation/62ADEC4D-3545-4E98-A612-0E6DF52CE525/d/iMCP.app/Contents/MacOS/imcp-server" and call initialize Note response is being parsed correctly and client.initialize() does not hang indefinitely * Fix warning: 'Cast from Client.AnyPendingRequest to unrelated type Client.PendingRequest<Any> always fails' * Fix warning: 'Conditional cast from NWError to NWError always succeeds' * Uncomment code in RoundtripTests * Formatting * Reorganize declarations * Inline JSONDecoder extension * Expand roundtrip tests --------- Co-authored-by: Sash Zats sash@zats.io <> Co-authored-by: Mattt Zmuda <mattt@loopwork.com>
123 lines
4.1 KiB
Swift
123 lines
4.1 KiB
Swift
import Foundation
|
|
import Logging
|
|
import SystemPackage
|
|
import Testing
|
|
|
|
@testable import MCP
|
|
|
|
@Suite("Roundtrip Tests")
|
|
struct RoundtripTests {
|
|
@Test(
|
|
.timeLimit(.minutes(1))
|
|
)
|
|
func testRoundtrip() async throws {
|
|
let (clientToServerRead, clientToServerWrite) = try FileDescriptor.pipe()
|
|
let (serverToClientRead, serverToClientWrite) = try FileDescriptor.pipe()
|
|
|
|
var logger = Logger(
|
|
label: "mcp.test.initialize",
|
|
factory: { StreamLogHandler.standardError(label: $0) })
|
|
logger.logLevel = .debug
|
|
|
|
let serverTransport = StdioTransport(
|
|
input: clientToServerRead,
|
|
output: serverToClientWrite,
|
|
logger: logger
|
|
)
|
|
let clientTransport = StdioTransport(
|
|
input: serverToClientRead,
|
|
output: clientToServerWrite,
|
|
logger: logger
|
|
)
|
|
|
|
let server = Server(
|
|
name: "TestServer",
|
|
version: "1.0.0",
|
|
capabilities: .init(prompts: .init(), tools: .init())
|
|
)
|
|
await server.withMethodHandler(ListTools.self) { _ in
|
|
return ListTools.Result(tools: [
|
|
Tool(
|
|
name: "add",
|
|
description: "Adds two numbers together",
|
|
inputSchema: [
|
|
"a": ["type": "integer", "description": "The first number"],
|
|
"a": ["type": "integer", "description": "The second number"],
|
|
])
|
|
])
|
|
}
|
|
await server.withMethodHandler(CallTool.self) { request in
|
|
guard request.name == "add" else {
|
|
return CallTool.Result(content: [.text("Invalid tool name")], isError: true)
|
|
}
|
|
|
|
guard let a = request.arguments?["a"]?.intValue,
|
|
let b = request.arguments?["b"]?.intValue
|
|
else {
|
|
return CallTool.Result(
|
|
content: [.text("Did not receive valid arguments")], isError: true)
|
|
}
|
|
|
|
return CallTool.Result(content: [.text("\(a + b)")], isError: false)
|
|
}
|
|
|
|
let client = Client(name: "TestClient", version: "1.0")
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
let initTask = Task {
|
|
let result = try await client.initialize()
|
|
|
|
#expect(result.serverInfo.name == "TestServer")
|
|
#expect(result.serverInfo.version == "1.0.0")
|
|
#expect(result.capabilities.prompts != nil)
|
|
#expect(result.capabilities.tools != nil)
|
|
#expect(result.protocolVersion == Version.latest)
|
|
}
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask {
|
|
try await Task.sleep(for: .seconds(1))
|
|
initTask.cancel()
|
|
throw CancellationError()
|
|
}
|
|
group.addTask {
|
|
try await initTask.value
|
|
}
|
|
try await group.next()
|
|
group.cancelAll()
|
|
}
|
|
|
|
let listToolsTask = Task {
|
|
let result = try await client.listTools()
|
|
#expect(result.count == 1)
|
|
#expect(result[0].name == "add")
|
|
}
|
|
|
|
let callToolTask = Task {
|
|
let result = try await client.callTool(name: "add", arguments: ["a": 1, "b": 2])
|
|
#expect(result.isError == false)
|
|
#expect(result.content == [.text("3")])
|
|
}
|
|
|
|
try await withThrowingTaskGroup(of: Void.self) { group in
|
|
group.addTask {
|
|
try await Task.sleep(for: .seconds(1))
|
|
listToolsTask.cancel()
|
|
throw CancellationError()
|
|
}
|
|
group.addTask {
|
|
try await callToolTask.value
|
|
}
|
|
try await group.next()
|
|
group.cancelAll()
|
|
}
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
try? clientToServerRead.close()
|
|
try? clientToServerWrite.close()
|
|
try? serverToClientRead.close()
|
|
try? serverToClientWrite.close()
|
|
}
|
|
}
|