swift-sdk/Tests/MCPTests/ServerTests.swift
Maksym Mova 6112a3995a
V0.11.x (#199)
* 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>
2026-02-19 13:09:31 +02:00

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()
}
}