Initial implementation

This commit is contained in:
Mattt Zmuda 2025-02-11 09:27:37 -08:00
commit be1e9583f8
No known key found for this signature in database
13 changed files with 4304 additions and 884 deletions

View file

@ -0,0 +1,124 @@
import Testing
@testable import MCP
@Suite("Client Tests")
struct ClientTests {
@Test("Client connect and disconnect")
func testClientConnectAndDisconnect() async throws {
let transport = MockTransport()
let client = Client(name: "TestClient", version: "1.0")
#expect(await transport.isConnected == false)
try await client.connect(transport: transport)
#expect(await transport.isConnected == true)
await client.disconnect()
#expect(await transport.isConnected == false)
}
@Test(
"Initialize request",
.timeLimit(.minutes(1))
)
func testClientInitialize() async throws {
let transport = MockTransport()
let client = Client(name: "TestClient", version: "1.0")
try await client.connect(transport: transport)
// Small delay to ensure message loop is started
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
// Create a task for initialize that we'll cancel
let initTask = Task {
try await client.initialize()
}
// Give it a moment to send the request
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
#expect(await transport.sentMessages.count == 1)
#expect(await transport.sentMessages[0].contains(Initialize.name))
#expect(await transport.sentMessages[0].contains(client.name))
#expect(await transport.sentMessages[0].contains(client.version))
// Cancel the initialize task
initTask.cancel()
// Disconnect client to clean up message loop and give time for continuation cleanup
await client.disconnect()
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
}
@Test(
"Ping request",
.timeLimit(.minutes(1))
)
func testClientPing() async throws {
let transport = MockTransport()
let client = Client(name: "TestClient", version: "1.0")
try await client.connect(transport: transport)
// Small delay to ensure message loop is started
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
// Create a task for the ping that we'll cancel
let pingTask = Task {
try await client.ping()
}
// Give it a moment to send the request
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
#expect(await transport.sentMessages.count == 1)
#expect(await transport.sentMessages[0].contains(Ping.name))
// Cancel the ping task
pingTask.cancel()
// Disconnect client to clean up message loop and give time for continuation cleanup
await client.disconnect()
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
}
@Test("Connection failure handling")
func testClientConnectionFailure() async {
let transport = MockTransport()
await transport.setFailConnect(true)
let client = Client(name: "TestClient", version: "1.0")
do {
try await client.connect(transport: transport)
#expect(Bool(false), "Expected connection to fail")
} catch let error as Error {
if case Error.transportError = error {
#expect(Bool(true))
} else {
#expect(Bool(false), "Expected transport error")
}
} catch {
#expect(Bool(false), "Expected MCP.Error")
}
}
@Test("Send failure handling")
func testClientSendFailure() async throws {
let transport = MockTransport()
await transport.setFailSend(true)
let client = Client(name: "TestClient", version: "1.0")
try await client.connect(transport: transport)
do {
try await client.ping()
#expect(Bool(false), "Expected ping to fail")
} catch let error as Error {
if case Error.transportError = error {
#expect(Bool(true))
} else {
#expect(Bool(false), "Expected transport error")
}
} catch {
#expect(Bool(false), "Expected MCP.Error")
}
}
}

View file

@ -0,0 +1,106 @@
import Foundation
import Logging
@testable import MCP
/// Mock transport for testing
actor MockTransport: Transport {
var logger: Logger
var isConnected = false
private(set) var sentMessages: [String] = []
private var messagesToReceive: [String] = []
private var messageStreamContinuation: AsyncThrowingStream<String, Swift.Error>.Continuation?
var shouldFailConnect = false
var shouldFailSend = false
init(logger: Logger = Logger(label: "mcp.test.transport")) {
self.logger = logger
}
func connect() async throws {
if shouldFailConnect {
throw Error.transportError(POSIXError(.ECONNREFUSED))
}
isConnected = true
}
func disconnect() async {
isConnected = false
messageStreamContinuation?.finish()
messageStreamContinuation = nil
}
func send<T: Encodable & Sendable>(_ message: T) async throws {
if shouldFailSend {
throw Error.transportError(POSIXError(.EIO))
}
let data = try JSONEncoder().encode(message)
let str = String(data: data, encoding: .utf8)!
sentMessages.append(str)
}
func receive() -> AsyncThrowingStream<String, Swift.Error> {
return AsyncThrowingStream<String, Swift.Error> { continuation in
messageStreamContinuation = continuation
// Send any queued messages
for message in messagesToReceive {
continuation.yield(message)
}
messagesToReceive.removeAll()
}
}
func queueRequest<M: MCP.Method>(_ request: Request<M>) throws {
let data = try JSONEncoder().encode(request)
let str = String(data: data, encoding: .utf8)!
if let continuation = messageStreamContinuation {
continuation.yield(str)
} else {
sentMessages.append(str)
}
}
func queueResponse<M: MCP.Method>(_ response: Response<M>) throws {
let data = try JSONEncoder().encode(response)
let str = String(data: data, encoding: .utf8)!
if let continuation = messageStreamContinuation {
continuation.yield(str)
} else {
messagesToReceive.append(str)
}
}
func queueNotification<N: MCP.Notification>(_ notification: Message<N>) throws {
let data = try JSONEncoder().encode(notification)
let str = String(data: data, encoding: .utf8)!
if let continuation = messageStreamContinuation {
continuation.yield(str)
} else {
messagesToReceive.append(str)
}
}
func getLastSentMessage<T: Decodable>() -> T? {
print("SENT:", sentMessages)
guard let lastMessage = sentMessages.last else { return nil }
do {
let data = lastMessage.data(using: .utf8)!
return try JSONDecoder().decode(T.self, from: data)
} catch {
return nil
}
}
func clearMessages() {
sentMessages.removeAll()
messagesToReceive.removeAll()
}
func setFailConnect(_ shouldFail: Bool) {
shouldFailConnect = shouldFail
}
func setFailSend(_ shouldFail: Bool) {
shouldFailSend = shouldFail
}
}

View file

@ -0,0 +1,51 @@
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
import struct Foundation.UUID
@testable import MCP
@Suite("ID Tests")
struct IDTests {
@Test("String ID initialization and encoding")
func testStringID() throws {
let id: ID = "test-id"
#expect(id.description == "test-id")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(id)
let decoded = try decoder.decode(ID.self, from: data)
#expect(decoded == id)
}
@Test("Number ID initialization and encoding")
func testNumberID() throws {
let id: ID = 42
#expect(id.description == "42")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(id)
let decoded = try decoder.decode(ID.self, from: data)
#expect(decoded == id)
}
@Test("Random ID generation")
func testRandomID() throws {
let id1 = ID.random
let id2 = ID.random
#expect(id1 != id2, "Random IDs should be unique")
if case .string(let str) = id1 {
#expect(!str.isEmpty)
// Verify it's a valid UUID string
#expect(UUID(uuidString: str) != nil)
} else {
#expect(Bool(false), "Random ID should be string type")
}
}
}

View file

@ -1,6 +0,0 @@
import Testing
@testable import MCP
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}

View file

@ -0,0 +1,89 @@
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
@testable import MCP
@Suite("Notification Tests")
struct NotificationTests {
struct TestNotification: Notification {
struct Parameters: Codable, Hashable, Sendable {
let event: String
}
static let name = "test.notification"
}
struct InitializedNotification: Notification {
static let name = "notifications/initialized"
}
@Test("Notification initialization with parameters")
func testNotificationWithParameters() throws {
let params = TestNotification.Parameters(event: "test-event")
let notification = TestNotification.message(params)
#expect(notification.method == TestNotification.name)
#expect(notification.params.event == "test-event")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(notification)
let decoded = try decoder.decode(Message<TestNotification>.self, from: data)
#expect(decoded.method == notification.method)
#expect(decoded.params.event == notification.params.event)
}
@Test("Empty parameters notification")
func testEmptyParametersNotification() throws {
struct EmptyNotification: Notification {
static let name = "empty.notification"
}
let notification = EmptyNotification.message()
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(notification)
let decoded = try decoder.decode(Message<EmptyNotification>.self, from: data)
#expect(decoded.method == notification.method)
}
@Test("Initialized notification encoding")
func testInitializedNotificationEncoding() throws {
let notification = InitializedNotification.message()
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(notification)
// Verify the exact JSON structure
let json = try JSONDecoder().decode([String: Value].self, from: data)
#expect(json["jsonrpc"] == "2.0")
#expect(json["method"] == "notifications/initialized")
#expect(json.count == 2, "Should only contain jsonrpc and method fields")
// Verify we can decode it back
let decoded = try decoder.decode(Message<InitializedNotification>.self, from: data)
#expect(decoded.method == InitializedNotification.name)
}
@Test("Initialized notification decoding")
func testInitializedNotificationDecoding() throws {
// Create a minimal JSON string
let jsonString = """
{"jsonrpc":"2.0","method":"notifications/initialized"}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode(Message<InitializedNotification>.self, from: data)
#expect(decoded.method == InitializedNotification.name)
}
}

View file

@ -0,0 +1,164 @@
import Foundation
import Testing
@testable import MCP
@Suite("Prompt Tests")
struct PromptTests {
@Test("Prompt initialization with valid parameters")
func testPromptInitialization() throws {
let argument = Prompt.Argument(
name: "test_arg",
description: "A test argument",
required: true
)
let prompt = Prompt(
name: "test_prompt",
description: "A test prompt",
arguments: [argument]
)
#expect(prompt.name == "test_prompt")
#expect(prompt.description == "A test prompt")
#expect(prompt.arguments?.count == 1)
#expect(prompt.arguments?[0].name == "test_arg")
#expect(prompt.arguments?[0].description == "A test argument")
#expect(prompt.arguments?[0].required == true)
}
@Test("Prompt Message encoding and decoding")
func testPromptMessageEncodingDecoding() throws {
let textMessage = Prompt.Message(
role: .user,
content: .text(text: "Hello, world!")
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(textMessage)
let decoded = try decoder.decode(Prompt.Message.self, from: data)
#expect(decoded.role == .user)
if case .text(let text) = decoded.content {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Prompt Message Content types encoding and decoding")
func testPromptMessageContentTypes() throws {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Test text content
let textContent = Prompt.Message.Content.text(text: "Test text")
let textData = try encoder.encode(textContent)
let decodedText = try decoder.decode(Prompt.Message.Content.self, from: textData)
if case .text(let text) = decodedText {
#expect(text == "Test text")
} else {
#expect(Bool(false), "Expected text content")
}
// Test image content
let imageContent = Prompt.Message.Content.image(data: "base64data", mimeType: "image/png")
let imageData = try encoder.encode(imageContent)
let decodedImage = try decoder.decode(Prompt.Message.Content.self, from: imageData)
if case .image(let data, let mimeType) = decodedImage {
#expect(data == "base64data")
#expect(mimeType == "image/png")
} else {
#expect(Bool(false), "Expected image content")
}
// Test resource content
let resourceContent = Prompt.Message.Content.resource(
uri: "file://test.txt",
mimeType: "text/plain",
text: "Sample text",
blob: "blob_data"
)
let resourceData = try encoder.encode(resourceContent)
let decodedResource = try decoder.decode(Prompt.Message.Content.self, from: resourceData)
if case .resource(let uri, let mimeType, let text, let blob) = decodedResource {
#expect(uri == "file://test.txt")
#expect(mimeType == "text/plain")
#expect(text == "Sample text")
#expect(blob == "blob_data")
} else {
#expect(Bool(false), "Expected resource content")
}
}
@Test("Prompt Reference validation")
func testPromptReference() throws {
let reference = Prompt.Reference(name: "test_prompt")
#expect(reference.name == "test_prompt")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(reference)
let decoded = try decoder.decode(Prompt.Reference.self, from: data)
#expect(decoded.name == "test_prompt")
}
@Test("GetPrompt parameters validation")
func testGetPromptParameters() throws {
let arguments: [String: Value] = [
"param1": .string("value1"),
"param2": .int(42),
]
let params = GetPrompt.Parameters(name: "test_prompt", arguments: arguments)
#expect(params.name == "test_prompt")
#expect(params.arguments?["param1"] == .string("value1"))
#expect(params.arguments?["param2"] == .int(42))
}
@Test("GetPrompt result validation")
func testGetPromptResult() throws {
let messages = [
Prompt.Message(role: .user, content: .text(text: "User message")),
Prompt.Message(role: .assistant, content: .text(text: "Assistant response")),
]
let result = GetPrompt.Result(description: "Test description", messages: messages)
#expect(result.description == "Test description")
#expect(result.messages.count == 2)
#expect(result.messages[0].role == .user)
#expect(result.messages[1].role == .assistant)
}
@Test("ListPrompts parameters validation")
func testListPromptsParameters() throws {
let params = ListPrompts.Parameters(cursor: "next_page")
#expect(params.cursor == "next_page")
let emptyParams = ListPrompts.Parameters()
#expect(emptyParams.cursor == nil)
}
@Test("ListPrompts result validation")
func testListPromptsResult() throws {
let prompts = [
Prompt(name: "prompt1", description: "First prompt"),
Prompt(name: "prompt2", description: "Second prompt"),
]
let result = ListPrompts.Result(prompts: prompts, nextCursor: "next_page")
#expect(result.prompts.count == 2)
#expect(result.prompts[0].name == "prompt1")
#expect(result.prompts[1].name == "prompt2")
#expect(result.nextCursor == "next_page")
}
@Test("PromptListChanged notification name validation")
func testPromptListChangedNotification() throws {
#expect(PromptListChangedNotification.name == "notifications/prompts/list_changed")
}
}

View file

@ -0,0 +1,79 @@
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
@testable import MCP
@Suite("Request Tests")
struct RequestTests {
struct TestMethod: Method {
struct Parameters: Codable, Hashable, Sendable {
let value: String
}
struct Result: Codable, Hashable, Sendable {
let success: Bool
}
static let name = "test.method"
}
struct EmptyMethod: Method {
static let name = "empty.method"
}
@Test("Request initialization with parameters")
func testRequestInitialization() throws {
let id: ID = "test-id"
let params = TestMethod.Parameters(value: "test")
let request = Request<TestMethod>(id: id, method: TestMethod.name, params: params)
#expect(request.id == id)
#expect(request.method == TestMethod.name)
#expect(request.params.value == "test")
}
@Test("Request encoding and decoding")
func testRequestEncodingDecoding() throws {
let request = TestMethod.request(id: "test-id", TestMethod.Parameters(value: "test"))
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(request)
let decoded = try decoder.decode(Request<TestMethod>.self, from: data)
#expect(decoded.id == request.id)
#expect(decoded.method == request.method)
#expect(decoded.params.value == request.params.value)
}
@Test("Empty parameters request encoding")
func testEmptyParametersRequestEncoding() throws {
let request = EmptyMethod.request(id: "test-id")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(request)
// Verify we can decode it back
let decoded = try decoder.decode(Request<EmptyMethod>.self, from: data)
#expect(decoded.id == request.id)
#expect(decoded.method == request.method)
}
@Test("Empty parameters request decoding")
func testEmptyParametersRequestDecoding() throws {
// Create a minimal JSON string
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","method":"empty.method"}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode(Request<EmptyMethod>.self, from: data)
#expect(decoded.id == "test-id")
#expect(decoded.method == EmptyMethod.name)
}
}

View file

@ -0,0 +1,138 @@
import Foundation
import Testing
@testable import MCP
@Suite("Resource Tests")
struct ResourceTests {
@Test("Resource initialization with valid parameters")
func testResourceInitialization() throws {
let resource = Resource(
name: "test_resource",
uri: "file://test.txt",
description: "A test resource",
mimeType: "text/plain",
metadata: ["key": "value"]
)
#expect(resource.name == "test_resource")
#expect(resource.uri == "file://test.txt")
#expect(resource.description == "A test resource")
#expect(resource.mimeType == "text/plain")
#expect(resource.metadata?["key"] == "value")
}
@Test("Resource encoding and decoding")
func testResourceEncodingDecoding() throws {
let resource = Resource(
name: "test_resource",
uri: "file://test.txt",
description: "Test resource description",
mimeType: "text/plain",
metadata: ["key1": "value1", "key2": "value2"]
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(resource)
let decoded = try decoder.decode(Resource.self, from: data)
#expect(decoded.name == resource.name)
#expect(decoded.uri == resource.uri)
#expect(decoded.description == resource.description)
#expect(decoded.mimeType == resource.mimeType)
#expect(decoded.metadata == resource.metadata)
}
@Test("Resource.Content text initialization and encoding")
func testResourceContentTextEncoding() throws {
let content = Resource.Content.text(
"Hello, world!", uri: "file://test.txt", mimeType: "text/plain")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(content)
let decoded = try decoder.decode(Resource.Content.self, from: data)
#expect(decoded.uri == "file://test.txt")
#expect(decoded.mimeType == "text/plain")
#expect(decoded.text == "Hello, world!")
#expect(decoded.blob == nil)
}
@Test("Resource.Content binary initialization and encoding")
func testResourceContentBinaryEncoding() throws {
let binaryData = "Test binary data".data(using: .utf8)!
let content = Resource.Content.binary(
binaryData, uri: "file://test.bin", mimeType: "application/octet-stream")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(content)
let decoded = try decoder.decode(Resource.Content.self, from: data)
#expect(decoded.uri == "file://test.bin")
#expect(decoded.mimeType == "application/octet-stream")
#expect(decoded.text == nil)
#expect(decoded.blob == binaryData.base64EncodedString())
}
@Test("ListResources parameters validation")
func testListResourcesParameters() throws {
let params = ListResources.Parameters(cursor: "next_page")
#expect(params.cursor == "next_page")
let emptyParams = ListResources.Parameters()
#expect(emptyParams.cursor == nil)
}
@Test("ListResources result validation")
func testListResourcesResult() throws {
let resources = [
Resource(name: "resource1", uri: "file://test1.txt"),
Resource(name: "resource2", uri: "file://test2.txt"),
]
let result = ListResources.Result(resources: resources, nextCursor: "next_page")
#expect(result.resources.count == 2)
#expect(result.resources[0].name == "resource1")
#expect(result.resources[1].name == "resource2")
#expect(result.nextCursor == "next_page")
}
@Test("ReadResource parameters validation")
func testReadResourceParameters() throws {
let params = ReadResource.Parameters(uri: "file://test.txt")
#expect(params.uri == "file://test.txt")
}
@Test("ReadResource result validation")
func testReadResourceResult() throws {
let contents = [
Resource.Content.text("Content 1", uri: "file://test1.txt"),
Resource.Content.text("Content 2", uri: "file://test2.txt"),
]
let result = ReadResource.Result(contents: contents)
#expect(result.contents.count == 2)
}
@Test("ResourceSubscribe parameters validation")
func testResourceSubscribeParameters() throws {
let params = ResourceSubscribe.Parameters(uri: "file://test.txt")
#expect(params.uri == "file://test.txt")
}
@Test("ResourceUpdatedNotification parameters validation")
func testResourceUpdatedNotification() throws {
let params = ResourceUpdatedNotification.Parameters(uri: "file://test.txt")
#expect(params.uri == "file://test.txt")
#expect(ResourceUpdatedNotification.name == "notifications/resources/updated")
}
@Test("ResourceListChangedNotification name validation")
func testResourceListChangedNotification() throws {
#expect(ResourceListChangedNotification.name == "notifications/resources/list_changed")
}
}

View file

@ -0,0 +1,119 @@
import Testing
import class Foundation.JSONDecoder
import class Foundation.JSONEncoder
@testable import MCP
@Suite("Response Tests")
struct ResponseTests {
struct TestMethod: Method {
struct Parameters: Codable, Hashable, Sendable {
let value: String
}
struct Result: Codable, Hashable, Sendable {
let success: Bool
}
static let name = "test.method"
}
struct EmptyMethod: Method {
static let name = "empty.method"
}
@Test("Success response initialization and encoding")
func testSuccessResponse() throws {
let id: ID = "test-id"
let result = TestMethod.Result(success: true)
let response = Response<TestMethod>(id: id, result: result)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(response)
let decoded = try decoder.decode(Response<TestMethod>.self, from: data)
if case .success(let decodedResult) = decoded.result {
#expect(decodedResult.success == true)
} else {
#expect(Bool(false), "Expected success result")
}
}
@Test("Error response initialization and encoding")
func testErrorResponse() throws {
let id: ID = "test-id"
let error = MCP.Error.parseError(nil)
let response = Response<TestMethod>(id: id, error: error)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(response)
let decoded = try decoder.decode(Response<TestMethod>.self, from: data)
if case .failure(let decodedError) = decoded.result {
#expect(decodedError.code == -32700)
#expect(
decodedError.localizedDescription
== "Parse error: Invalid JSON: Parse error: Invalid JSON")
} else {
#expect(Bool(false), "Expected error result")
}
}
@Test("Error response with detail")
func testErrorResponseWithDetail() throws {
let id: ID = "test-id"
let error = MCP.Error.parseError("Invalid syntax")
let response = Response<TestMethod>(id: id, error: error)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(response)
let decoded = try decoder.decode(Response<TestMethod>.self, from: data)
if case .failure(let decodedError) = decoded.result {
#expect(decodedError.code == -32700)
#expect(
decodedError.localizedDescription
== "Parse error: Invalid JSON: Parse error: Invalid JSON: Invalid syntax")
} else {
#expect(Bool(false), "Expected error result")
}
}
@Test("Empty result response encoding")
func testEmptyResultResponseEncoding() throws {
let response = EmptyMethod.response(id: "test-id")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(response)
// Verify we can decode it back
let decoded = try decoder.decode(Response<EmptyMethod>.self, from: data)
#expect(decoded.id == response.id)
}
@Test("Empty result response decoding")
func testEmptyResultResponseDecoding() throws {
// Create a minimal JSON string
let jsonString = """
{"jsonrpc":"2.0","id":"test-id","result":{}}
"""
let data = jsonString.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try decoder.decode(Response<EmptyMethod>.self, from: data)
#expect(decoded.id == "test-id")
if case .success = decoded.result {
// Success
} else {
#expect(Bool(false), "Expected success result")
}
}
}

View file

@ -0,0 +1,73 @@
import Foundation
import Logging
import SystemPackage
import Testing
@testable import MCP
@Suite("Roundtrip Tests")
struct RoundtripTests {
@Test(
"Initialize roundtrip",
.timeLimit(.minutes(1))
)
func testInitializeRoundtrip() 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())
)
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()
}
await server.stop()
await client.disconnect()
try? clientToServerRead.close()
try? clientToServerWrite.close()
try? serverToClientRead.close()
try? serverToClientWrite.close()
}
}

View file

@ -0,0 +1,51 @@
import Foundation
import Testing
@testable import MCP
@Suite("Server Tests")
struct ServerTests {
@Test("Start and stop server")
func testServerStartAndStop() async throws {
let transport = MockTransport()
let server = Server(name: "TestServer", version: "1.0")
#expect(await transport.isConnected == false)
try await server.start(transport: transport)
#expect(await transport.isConnected == true)
await server.stop()
#expect(await transport.isConnected == false)
}
@Test("Initialize request handling")
func testServerHandleInitialize() async throws {
let transport = MockTransport()
// Queue an initialize request
try await transport.queueRequest(
Initialize.request(
.init(
protocolVersion: Version.latest,
capabilities: .init(),
clientInfo: .init(name: "TestClient", version: "1.0")
)
))
// Start the server
let server: Server = Server(
name: "TestServer",
version: "1.0"
)
try await server.start(transport: transport)
// Wait for message processing and response
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
#expect(await transport.sentMessages.count == 1)
#expect(await transport.sentMessages[0].contains(Initialize.name))
// Clean up
await server.stop()
await transport.disconnect()
}
}

View file

@ -0,0 +1,164 @@
import Foundation
import Testing
@testable import MCP
@Suite("Tool Tests")
struct ToolTests {
@Test("Tool initialization with valid parameters")
func testToolInitialization() throws {
let tool = Tool(
name: "test_tool",
description: "A test tool",
inputSchema: .object([
"param1": .string("Test parameter")
])
)
#expect(tool.name == "test_tool")
#expect(tool.description == "A test tool")
#expect(tool.inputSchema != nil)
}
@Test("Tool encoding and decoding")
func testToolEncodingDecoding() throws {
let tool = Tool(
name: "test_tool",
description: "Test tool description",
inputSchema: .object([
"param1": .string("String parameter"),
"param2": .int(42),
])
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(tool)
let decoded = try decoder.decode(Tool.self, from: data)
#expect(decoded.name == tool.name)
#expect(decoded.description == tool.description)
#expect(decoded.inputSchema == tool.inputSchema)
}
@Test("Text content encoding and decoding")
func testToolContentTextEncoding() throws {
let content = Tool.Content.text("Hello, world!")
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(content)
let decoded = try decoder.decode(Tool.Content.self, from: data)
if case .text(let text) = decoded {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("Image content encoding and decoding")
func testToolContentImageEncoding() throws {
let content = Tool.Content.image(
data: "base64data",
mimeType: "image/png",
metadata: ["width": "100", "height": "100"]
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(content)
let decoded = try decoder.decode(Tool.Content.self, from: data)
if case .image(let data, let mimeType, let metadata) = decoded {
#expect(data == "base64data")
#expect(mimeType == "image/png")
#expect(metadata?["width"] == "100")
#expect(metadata?["height"] == "100")
} else {
#expect(Bool(false), "Expected image content")
}
}
@Test("Resource content encoding and decoding")
func testToolContentResourceEncoding() throws {
let content = Tool.Content.resource(
uri: "file://test.txt",
mimeType: "text/plain",
text: "Sample text"
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(content)
let decoded = try decoder.decode(Tool.Content.self, from: data)
if case .resource(let uri, let mimeType, let text) = decoded {
#expect(uri == "file://test.txt")
#expect(mimeType == "text/plain")
#expect(text == "Sample text")
} else {
#expect(Bool(false), "Expected resource content")
}
}
@Test("ListTools parameters validation")
func testListToolsParameters() throws {
let params = ListTools.Parameters(cursor: "next_page")
#expect(params.cursor == "next_page")
let emptyParams = ListTools.Parameters()
#expect(emptyParams.cursor == nil)
}
@Test("ListTools result validation")
func testListToolsResult() throws {
let tools = [
Tool(name: "tool1", description: "First tool", inputSchema: nil),
Tool(name: "tool2", description: "Second tool", inputSchema: nil),
]
let result = ListTools.Result(tools: tools, nextCursor: "next_page")
#expect(result.tools.count == 2)
#expect(result.tools[0].name == "tool1")
#expect(result.tools[1].name == "tool2")
#expect(result.nextCursor == "next_page")
}
@Test("CallTool parameters validation")
func testCallToolParameters() throws {
let arguments: [String: Value] = [
"param1": .string("value1"),
"param2": .int(42),
]
let params = CallTool.Parameters(name: "test_tool", arguments: arguments)
#expect(params.name == "test_tool")
#expect(params.arguments?["param1"] == .string("value1"))
#expect(params.arguments?["param2"] == .int(42))
}
@Test("CallTool result validation")
func testCallToolResult() throws {
let content = [
Tool.Content.text("Result 1"),
Tool.Content.text("Result 2"),
]
let result = CallTool.Result(content: content, isError: false)
#expect(result.content.count == 2)
#expect(result.isError == false)
if case .text(let text) = result.content[0] {
#expect(text == "Result 1")
} else {
#expect(Bool(false), "Expected text content")
}
}
@Test("ToolListChanged notification name validation")
func testToolListChangedNotification() throws {
#expect(ToolListChangedNotification.name == "notifications/tools/list_changed")
}
}

View file

@ -0,0 +1,122 @@
import Foundation
import SystemPackage
import Testing
@testable import MCP
@Suite("Stdio Transport Tests")
struct StdioTransportTests {
@Test("Connection")
func testStdioTransportConnection() async throws {
let (input, _) = try FileDescriptor.pipe()
let (_, output) = try FileDescriptor.pipe()
let transport = StdioTransport(input: input, output: output, logger: nil)
try await transport.connect()
await transport.disconnect()
}
@Test("Send Message")
func testStdioTransportSendMessage() async throws {
let (reader, output) = try FileDescriptor.pipe()
let (input, _) = try FileDescriptor.pipe()
let transport = StdioTransport(input: input, output: output, logger: nil)
try await transport.connect()
// Test sending a simple message
let message = #"{"key":"value"}"#
try await transport.send(message)
// Read and verify the output
var buffer = [UInt8](repeating: 0, count: 1024)
let bytesRead = try buffer.withUnsafeMutableBufferPointer { pointer in
try reader.read(into: UnsafeMutableRawBufferPointer(pointer))
}
let data = Data(buffer[..<bytesRead])
let expectedOutput = message.data(using: .utf8)! + "\n".data(using: .utf8)!
#expect(data == expectedOutput)
await transport.disconnect()
}
@Test("Receive Message")
func testStdioTransportReceiveMessage() async throws {
let (input, writer) = try FileDescriptor.pipe()
let (_, output) = try FileDescriptor.pipe()
let transport = StdioTransport(input: input, output: output, logger: nil)
try await transport.connect()
// Write test message to input pipe
let message = ["key": "value"]
let messageData = try JSONEncoder().encode(message) + "\n".data(using: .utf8)!
try writer.writeAll(messageData)
try writer.close()
// Start receiving messages
let stream: AsyncThrowingStream<String, Swift.Error> = await transport.receive()
var iterator = stream.makeAsyncIterator()
// Get first message
let received = try await iterator.next()
#expect(received == #"{"key":"value"}"#)
await transport.disconnect()
}
@Test("Invalid JSON")
func testStdioTransportInvalidJSON() async throws {
let (input, writer) = try FileDescriptor.pipe()
let (_, output) = try FileDescriptor.pipe()
let transport = StdioTransport(input: input, output: output, logger: nil)
try await transport.connect()
// Write invalid JSON to input pipe
let invalidJSON = #"{ invalid json }"#
try writer.writeAll(invalidJSON.data(using: .utf8)!)
try writer.close()
let stream: AsyncThrowingStream<String, Swift.Error> = await transport.receive()
var iterator = stream.makeAsyncIterator()
_ = try await iterator.next()
await transport.disconnect()
}
@Test("Send Error")
func testStdioTransportSendError() async throws {
let (input, _) = try FileDescriptor.pipe()
let transport = StdioTransport(
input: input,
output: FileDescriptor(rawValue: -1), // Invalid fd
logger: nil
)
do {
try await transport.connect()
#expect(Bool(false), "Expected connect to throw an error")
} catch {
#expect(error is MCP.Error)
}
await transport.disconnect()
}
@Test("Receive Error")
func testStdioTransportReceiveError() async throws {
let (_, output) = try FileDescriptor.pipe()
let transport = StdioTransport(
input: FileDescriptor(rawValue: -1), // Invalid fd
output: output,
logger: nil
)
do {
try await transport.connect()
#expect(Bool(false), "Expected connect to throw an error")
} catch {
#expect(error is MCP.Error)
}
await transport.disconnect()
}
}