* Support 2025-06-18 Spec (#167) * Support OpenAI _meta for Tool * Update init * Add title and outputSchema * Concurreny issue * General fields * Add Generate Fields to Server elements * 4 tab spacing * Update Version * Elicitation * Format * More tests and inits * Elicitation in README * Fix orphaned test * Add title to more structs * Public init * Fix meta validation and make tests more generic * Format and make Result properties public again * Revert more visibility changes * feat: 2025-11-25 phase 1, includes Version, Icon, Progress (#181) * chore: fixed unit tests * feat: added icon and progess base structs * feat: added progress notification and meta to callTool * feat: added tests * feat: simplified RequestMeta encoding/decoding * tests: added unit test to track progress * chore: rebased v0.11.x * feat: implemented cancellation * feat: added overloaded callTool method * fix: encoding Response _meta fix * chore: added old version to the versions list * feat: 2025-11-25 phase 2; added Logging and Completions (#196) * feat: added Logging and Completions * feat: updated EmbeddedResource for the latest spec * chore: changed encoding of meta in Prompts to encodeIfPresent * feat: compliance tests + http server transport (#197) * feat: http server transport * feat: added http server transport * feat: removed unused files * feat: added conformance tests * feat: improved conformace testing * chore: removed redundant files * feat/2025-11-25: sampling, elicitation, roots (#198) * feat: Roots implementation * feat: added unit testing * feat: improved unit tests and fixed NetworkTransport crash * feat: implemented sampling and elicitation * feat: improved sampling tests * feat: improved elicitation tests * feat: improved confirmance tests * feat: improved Prompts unit tests * feat: removed test code * feat: updated conformance tests * chore: removed redundant commas * chore: removed 6.0.3 swift version from ci * chore: improved unit tests a little bit * chore: update README.md * chore: add server progress tracking example to README.md * chore: updated README.md * chore: updated transports table formatting * chore: updated MockNetworkConnection --------- Co-authored-by: Austin Evans <austinjevans@me.com> Co-authored-by: yehorsobko <yehorsobko@macpaw.com>
219 lines
7 KiB
Swift
219 lines
7 KiB
Swift
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.queue(
|
|
request: 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(for: .milliseconds(200))
|
|
|
|
#expect(await transport.sentMessages.count == 1)
|
|
|
|
let messages = await transport.sentMessages
|
|
if let response = messages.first {
|
|
#expect(response.contains("serverInfo"))
|
|
}
|
|
|
|
// Clean up
|
|
await server.stop()
|
|
await transport.disconnect()
|
|
}
|
|
|
|
@Test("Initialize hook - successful")
|
|
func testInitializeHookSuccess() async throws {
|
|
let transport = MockTransport()
|
|
|
|
actor TestState {
|
|
var hookCalled = false
|
|
func setHookCalled() { hookCalled = true }
|
|
func wasHookCalled() -> Bool { hookCalled }
|
|
}
|
|
|
|
let state = TestState()
|
|
let server = Server(name: "TestServer", version: "1.0")
|
|
|
|
// Start with the hook directly
|
|
try await server.start(transport: transport) { clientInfo, capabilities in
|
|
#expect(clientInfo.name == "TestClient")
|
|
#expect(clientInfo.version == "1.0")
|
|
await state.setHookCalled()
|
|
}
|
|
|
|
// Wait for server to initialize
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
|
|
// Queue an initialize request
|
|
try await transport.queue(
|
|
request: Initialize.request(
|
|
.init(
|
|
protocolVersion: Version.latest,
|
|
capabilities: .init(),
|
|
clientInfo: .init(name: "TestClient", version: "1.0")
|
|
)
|
|
))
|
|
|
|
// Wait for message processing and hook execution
|
|
try await Task.sleep(for: .milliseconds(500))
|
|
|
|
#expect(await state.wasHookCalled() == true)
|
|
#expect(await transport.sentMessages.count >= 1)
|
|
|
|
let messages = await transport.sentMessages
|
|
if let response = messages.first {
|
|
#expect(response.contains("serverInfo"))
|
|
}
|
|
|
|
await server.stop()
|
|
await transport.disconnect()
|
|
}
|
|
|
|
@Test("Initialize hook - rejection")
|
|
func testInitializeHookRejection() async throws {
|
|
let transport = MockTransport()
|
|
|
|
let server = Server(name: "TestServer", version: "1.0")
|
|
|
|
try await server.start(transport: transport) { clientInfo, _ in
|
|
if clientInfo.name == "BlockedClient" {
|
|
throw MCPError.invalidRequest("Client not allowed")
|
|
}
|
|
}
|
|
|
|
// Wait for server to initialize
|
|
try await Task.sleep(for: .milliseconds(10))
|
|
|
|
// Queue an initialize request from blocked client
|
|
try await transport.queue(
|
|
request: Initialize.request(
|
|
.init(
|
|
protocolVersion: Version.latest,
|
|
capabilities: .init(),
|
|
clientInfo: .init(name: "BlockedClient", version: "1.0")
|
|
)
|
|
))
|
|
|
|
// Wait for message processing
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
#expect(await transport.sentMessages.count >= 1)
|
|
|
|
let messages = await transport.sentMessages
|
|
if let response = messages.first {
|
|
#expect(response.contains("error"))
|
|
#expect(response.contains("Client not allowed"))
|
|
}
|
|
|
|
await server.stop()
|
|
await transport.disconnect()
|
|
}
|
|
|
|
@Test("JSON-RPC batch processing")
|
|
func testJSONRPCBatchProcessing() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
let server = Server(name: "TestServer", version: "1.0")
|
|
|
|
// Connect transports
|
|
try await clientTransport.connect()
|
|
try await serverTransport.connect()
|
|
|
|
// Start receiving messages on client side
|
|
let receiveTask = Task {
|
|
var responses: [String] = []
|
|
for try await data in await clientTransport.receive() {
|
|
if let response = String(data: data, encoding: .utf8) {
|
|
responses.append(response)
|
|
}
|
|
// Stop after receiving 2 responses (initialize + batch)
|
|
if responses.count == 2 {
|
|
break
|
|
}
|
|
}
|
|
return responses
|
|
}
|
|
|
|
// Start the server
|
|
try await server.start(transport: serverTransport)
|
|
|
|
// Initialize the server first
|
|
let initRequest = Initialize.request(
|
|
.init(
|
|
protocolVersion: Version.latest,
|
|
capabilities: .init(),
|
|
clientInfo: .init(name: "TestClient", version: "1.0")
|
|
)
|
|
)
|
|
let initData = try JSONEncoder().encode(AnyRequest(initRequest))
|
|
try await clientTransport.send(initData)
|
|
|
|
// Wait for initialization
|
|
try await Task.sleep(for: .milliseconds(100))
|
|
|
|
// Create a batch with multiple requests
|
|
let batchJSON = """
|
|
[
|
|
{"jsonrpc":"2.0","id":1,"method":"ping","params":{}},
|
|
{"jsonrpc":"2.0","id":2,"method":"ping","params":{}}
|
|
]
|
|
"""
|
|
let batchData = batchJSON.data(using: .utf8)!
|
|
try await clientTransport.send(batchData)
|
|
|
|
// Wait for batch processing
|
|
try await Task.sleep(for: .milliseconds(200))
|
|
|
|
// Get responses
|
|
let responses = try await receiveTask.value
|
|
#expect(responses.count == 2)
|
|
|
|
// Verify the batch response (second response)
|
|
if responses.count >= 2 {
|
|
let batchResponse = responses[1]
|
|
|
|
// Should be an array
|
|
#expect(batchResponse.hasPrefix("["))
|
|
#expect(batchResponse.hasSuffix("]"))
|
|
|
|
// Should contain both request IDs
|
|
#expect(batchResponse.contains("\"id\":1"))
|
|
#expect(batchResponse.contains("\"id\":2"))
|
|
}
|
|
|
|
await server.stop()
|
|
await clientTransport.disconnect()
|
|
await serverTransport.disconnect()
|
|
}
|
|
}
|