* 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
215 lines
7.3 KiB
Swift
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()
|
|
}
|
|
}
|