Initial implementation
This commit is contained in:
parent
cc603d35fe
commit
be1e9583f8
13 changed files with 4304 additions and 884 deletions
124
Tests/MCPTests/ClientTests.swift
Normal file
124
Tests/MCPTests/ClientTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Tests/MCPTests/Helpers/MockTransport.swift
Normal file
106
Tests/MCPTests/Helpers/MockTransport.swift
Normal 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
|
||||
}
|
||||
}
|
||||
51
Tests/MCPTests/IDTests.swift
Normal file
51
Tests/MCPTests/IDTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
}
|
||||
89
Tests/MCPTests/NotificationTests.swift
Normal file
89
Tests/MCPTests/NotificationTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
164
Tests/MCPTests/PromptTests.swift
Normal file
164
Tests/MCPTests/PromptTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
79
Tests/MCPTests/RequestTests.swift
Normal file
79
Tests/MCPTests/RequestTests.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
138
Tests/MCPTests/ResourceTests.swift
Normal file
138
Tests/MCPTests/ResourceTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
119
Tests/MCPTests/ResponseTests.swift
Normal file
119
Tests/MCPTests/ResponseTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Tests/MCPTests/RoundtripTests.swift
Normal file
73
Tests/MCPTests/RoundtripTests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
51
Tests/MCPTests/ServerTests.swift
Normal file
51
Tests/MCPTests/ServerTests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
164
Tests/MCPTests/ToolTests.swift
Normal file
164
Tests/MCPTests/ToolTests.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
122
Tests/MCPTests/TransportTests.swift
Normal file
122
Tests/MCPTests/TransportTests.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue