swift-sdk/Tests/MCPTests/RoundtripTests.swift
yehorsobko 973b46b8f7
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
2026-03-03 17:33:28 +02:00

215 lines
7.3 KiB
Swift

import Foundation
import Logging
import Testing
@testable import MCP
#if canImport(System)
import System
#else
@preconcurrency import SystemPackage
#endif
@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(),
resources: .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(text: "Invalid tool name", annotations: nil, _meta: nil)], isError: true)
}
guard let a = request.arguments?["a"]?.intValue,
let b = request.arguments?["b"]?.intValue
else {
return CallTool.Result(
content: [.text(text: "Did not receive valid arguments", annotations: nil, _meta: nil)], isError: true)
}
return CallTool.Result(content: [.text(text: "\(a + b)", annotations: nil, _meta: nil)])
}
// Add resource handlers to server
await server.withMethodHandler(ListResources.self) { _ in
return ListResources.Result(resources: [
Resource(
name: "Example Text",
uri: "test://example.txt",
description: "A test resource",
mimeType: "text/plain"
),
Resource(
name: "Test Data",
uri: "test://data.json",
description: "JSON test data",
mimeType: "application/json"
),
])
}
await server.withMethodHandler(ReadResource.self) { request in
guard request.uri == "test://example.txt" else {
return ReadResource.Result(contents: [.text("Resource not found", uri: request.uri)]
)
}
return ReadResource.Result(contents: [.text("Hello, World!", uri: request.uri)])
}
let client = Client(name: "TestClient", version: "1.0")
try await server.start(transport: serverTransport)
let initTask = Task {
let result = try await client.connect(transport: clientTransport)
#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()
}
// Test ping
let pingTask = Task {
try await client.ping()
// Ping doesn't return anything, so just getting here without throwing is success
#expect(Bool(true))
}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await Task.sleep(for: .seconds(1))
pingTask.cancel()
throw CancellationError()
}
group.addTask {
try await pingTask.value
}
try await group.next()
group.cancelAll()
}
let listToolsTask = Task {
let (tools, _) = try await client.listTools()
#expect(tools.count == 1)
#expect(tools[0].name == "add")
}
let callToolTask = Task {
let result = try await client.callTool(name: "add", arguments: ["a": 1, "b": 2])
#expect(result.isError == nil)
#expect(result.content == [.text(text: "3", annotations: nil, _meta: nil)])
}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await Task.sleep(for: .seconds(1))
listToolsTask.cancel()
callToolTask.cancel()
throw CancellationError()
}
group.addTask {
try await listToolsTask.value
}
group.addTask {
try await callToolTask.value
}
try await group.next()
group.cancelAll()
}
// Test listing resources
let listResourcesTask = Task {
let result = try await client.listResources()
#expect(result.resources.count == 2)
#expect(result.resources[0].uri == "test://example.txt")
#expect(result.resources[0].name == "Example Text")
#expect(result.resources[1].uri == "test://data.json")
#expect(result.resources[1].name == "Test Data")
}
// Test reading a resource
let readResourceTask = Task {
let result = try await client.readResource(uri: "test://example.txt")
#expect(result.count == 1)
#expect(result[0] == .text("Hello, World!", uri: "test://example.txt"))
}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask {
try await Task.sleep(for: .seconds(1))
listResourcesTask.cancel()
readResourceTask.cancel()
throw CancellationError()
}
group.addTask {
try await listResourcesTask.value
}
group.addTask {
try await readResourceTask.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()
}
}