* fix: review fixes of the latest changes * fix: updated unit tests * chore: more conformance compliance improvements * chore: reverted protocol changes * chore: more compliance improvements * chore: more conformance compliance improvements * chore: improvements to the backcompat and updated to Tool unit tests * chore: ci update
1093 lines
39 KiB
Swift
1093 lines
39 KiB
Swift
import Logging
|
|
import Testing
|
|
|
|
import class Foundation.JSONDecoder
|
|
import class Foundation.JSONEncoder
|
|
|
|
@testable import MCP
|
|
|
|
@Suite("Sampling Tests")
|
|
struct SamplingTests {
|
|
@Test("Sampling.Message encoding and decoding")
|
|
func testSamplingMessageCoding() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
// Test text content
|
|
let textMessage: Sampling.Message = .user("Hello, world!")
|
|
|
|
let textData = try encoder.encode(textMessage)
|
|
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
|
|
|
|
#expect(decodedTextMessage.role == .user)
|
|
if case .single(.text(let text)) = decodedTextMessage.content {
|
|
#expect(text == "Hello, world!")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test image content
|
|
let imageMessage: Sampling.Message = .assistant(
|
|
.image(data: "base64imagedata", mimeType: "image/png"))
|
|
|
|
let imageData = try encoder.encode(imageMessage)
|
|
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
|
|
|
|
#expect(decodedImageMessage.role == .assistant)
|
|
if case .single(.image(let data, let mimeType)) = decodedImageMessage.content {
|
|
#expect(data == "base64imagedata")
|
|
#expect(mimeType == "image/png")
|
|
} else {
|
|
#expect(Bool(false), "Expected image content")
|
|
}
|
|
}
|
|
|
|
@Test("ModelPreferences encoding and decoding")
|
|
func testModelPreferencesCoding() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let preferences = Sampling.ModelPreferences(
|
|
hints: [
|
|
Sampling.ModelPreferences.Hint(name: "claude-4"),
|
|
Sampling.ModelPreferences.Hint(name: "gpt-4.1"),
|
|
],
|
|
costPriority: 0.8,
|
|
speedPriority: 0.3,
|
|
intelligencePriority: 0.9
|
|
)
|
|
|
|
let data = try encoder.encode(preferences)
|
|
let decoded = try decoder.decode(Sampling.ModelPreferences.self, from: data)
|
|
|
|
#expect(decoded.hints?.count == 2)
|
|
#expect(decoded.hints?[0].name == "claude-4")
|
|
#expect(decoded.hints?[1].name == "gpt-4.1")
|
|
#expect(decoded.costPriority?.doubleValue == 0.8)
|
|
#expect(decoded.speedPriority?.doubleValue == 0.3)
|
|
#expect(decoded.intelligencePriority?.doubleValue == 0.9)
|
|
}
|
|
|
|
@Test("ContextInclusion encoding and decoding")
|
|
func testContextInclusionCoding() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let contexts: [Sampling.ContextInclusion] = [.none, .thisServer, .allServers]
|
|
|
|
for context in contexts {
|
|
let data = try encoder.encode(context)
|
|
let decoded = try decoder.decode(Sampling.ContextInclusion.self, from: data)
|
|
#expect(decoded == context)
|
|
}
|
|
}
|
|
|
|
@Test("StopReason encoding and decoding")
|
|
func testStopReasonCoding() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let reasons: [Sampling.StopReason] = [.endTurn, .stopSequence, .maxTokens]
|
|
|
|
for reason in reasons {
|
|
let data = try encoder.encode(reason)
|
|
let decoded = try decoder.decode(Sampling.StopReason.self, from: data)
|
|
#expect(decoded == reason)
|
|
}
|
|
}
|
|
|
|
@Test("CreateMessage request parameters")
|
|
func testCreateMessageParameters() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let messages: [Sampling.Message] = [
|
|
.user("What is the weather like?"),
|
|
.assistant("I need to check the weather for you."),
|
|
]
|
|
|
|
let modelPreferences = Sampling.ModelPreferences(
|
|
hints: [Sampling.ModelPreferences.Hint(name: "claude-4-sonnet")],
|
|
costPriority: 0.5,
|
|
speedPriority: 0.7,
|
|
intelligencePriority: 0.9
|
|
)
|
|
|
|
let parameters = CreateSamplingMessage.Parameters(
|
|
messages: messages,
|
|
modelPreferences: modelPreferences,
|
|
systemPrompt: "You are a helpful weather assistant.",
|
|
includeContext: .thisServer,
|
|
temperature: 0.7,
|
|
maxTokens: 150,
|
|
stopSequences: ["END", "STOP"],
|
|
_meta: Metadata(additionalFields: ["provider": "test"])
|
|
)
|
|
|
|
let data = try encoder.encode(parameters)
|
|
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
|
|
|
|
#expect(decoded.messages.count == 2)
|
|
#expect(decoded.messages[0].role == .user)
|
|
#expect(decoded.systemPrompt == "You are a helpful weather assistant.")
|
|
#expect(decoded.includeContext == .thisServer)
|
|
#expect(decoded.temperature == 0.7)
|
|
#expect(decoded.maxTokens == 150)
|
|
#expect(decoded.stopSequences?.count == 2)
|
|
#expect(decoded.stopSequences?[0] == "END")
|
|
#expect(decoded.stopSequences?[1] == "STOP")
|
|
#expect(decoded._meta?["provider"]?.stringValue == "test")
|
|
}
|
|
|
|
@Test("CreateMessage result")
|
|
func testCreateMessageResult() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let result = CreateSamplingMessage.Result(
|
|
model: "claude-4-sonnet",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text("The weather is sunny and 75°F.")
|
|
)
|
|
|
|
let data = try encoder.encode(result)
|
|
let decoded = try decoder.decode(CreateSamplingMessage.Result.self, from: data)
|
|
|
|
#expect(decoded.model == "claude-4-sonnet")
|
|
#expect(decoded.stopReason == .endTurn)
|
|
#expect(decoded.role == .assistant)
|
|
|
|
if case .single(.text(let text)) = decoded.content {
|
|
#expect(text == "The weather is sunny and 75°F.")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
}
|
|
|
|
@Test("CreateMessage request creation")
|
|
func testCreateMessageRequest() throws {
|
|
let messages: [Sampling.Message] = [
|
|
.user("Hello")
|
|
]
|
|
|
|
let request = CreateSamplingMessage.request(
|
|
.init(
|
|
messages: messages,
|
|
maxTokens: 100
|
|
)
|
|
)
|
|
|
|
#expect(request.method == "sampling/createMessage")
|
|
#expect(request.params.messages.count == 1)
|
|
#expect(request.params.maxTokens == 100)
|
|
}
|
|
|
|
@Test("Client capabilities include sampling")
|
|
func testClientCapabilitiesIncludeSampling() throws {
|
|
let capabilities = Client.Capabilities(
|
|
sampling: .init()
|
|
)
|
|
|
|
#expect(capabilities.sampling != nil)
|
|
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let data = try encoder.encode(capabilities)
|
|
let decoded = try decoder.decode(Client.Capabilities.self, from: data)
|
|
|
|
#expect(decoded.sampling != nil)
|
|
}
|
|
|
|
@Test("Client sampling handler registration")
|
|
func testClientSamplingHandlerRegistration() async throws {
|
|
let client = Client(name: "TestClient", version: "1.0")
|
|
|
|
// Test that sampling handler can be registered
|
|
let handlerClient = await client.withSamplingHandler { parameters in
|
|
// Mock handler that returns a simple response
|
|
return CreateSamplingMessage.Result(
|
|
model: "test-model",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text("Test response")
|
|
)
|
|
}
|
|
|
|
// Should return self for method chaining
|
|
#expect(handlerClient === client)
|
|
}
|
|
|
|
@Test("Sampling message content JSON format")
|
|
func testSamplingMessageContentJSONFormat() throws {
|
|
let encoder = JSONEncoder()
|
|
encoder.outputFormatting = [.sortedKeys]
|
|
|
|
// Test text content JSON format
|
|
let textContent: Sampling.Message.Content = .text("Hello")
|
|
let textData = try encoder.encode(textContent)
|
|
let textJSON = String(data: textData, encoding: .utf8)!
|
|
|
|
#expect(textJSON.contains("\"type\":\"text\""))
|
|
#expect(textJSON.contains("\"text\":\"Hello\""))
|
|
|
|
// Test image content JSON format
|
|
let imageContent: Sampling.Message.Content = .image(
|
|
data: "base64data", mimeType: "image/png")
|
|
let imageData = try encoder.encode(imageContent)
|
|
let imageJSON = String(data: imageData, encoding: .utf8)!
|
|
|
|
#expect(imageJSON.contains("\"type\":\"image\""))
|
|
#expect(imageJSON.contains("\"data\":\"base64data\""))
|
|
#expect(imageJSON.contains("\"mimeType\":\"image\\/png\""))
|
|
}
|
|
|
|
@Test("UnitInterval in Sampling.ModelPreferences")
|
|
func testUnitIntervalInModelPreferences() throws {
|
|
// Test that UnitInterval validation works in Sampling.ModelPreferences
|
|
let validPreferences = Sampling.ModelPreferences(
|
|
costPriority: 0.5,
|
|
speedPriority: 1.0,
|
|
intelligencePriority: 0.0
|
|
)
|
|
|
|
#expect(validPreferences.costPriority?.doubleValue == 0.5)
|
|
#expect(validPreferences.speedPriority?.doubleValue == 1.0)
|
|
#expect(validPreferences.intelligencePriority?.doubleValue == 0.0)
|
|
|
|
// Test JSON encoding/decoding preserves UnitInterval constraints
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let data = try encoder.encode(validPreferences)
|
|
let decoded = try decoder.decode(Sampling.ModelPreferences.self, from: data)
|
|
|
|
#expect(decoded.costPriority?.doubleValue == 0.5)
|
|
#expect(decoded.speedPriority?.doubleValue == 1.0)
|
|
#expect(decoded.intelligencePriority?.doubleValue == 0.0)
|
|
}
|
|
|
|
@Test("Message factory methods")
|
|
func testMessageFactoryMethods() throws {
|
|
// Test user message factory method
|
|
let userMessage: Sampling.Message = .user("Hello, world!")
|
|
#expect(userMessage.role == .user)
|
|
if case .single(.text(let text)) = userMessage.content {
|
|
#expect(text == "Hello, world!")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test assistant message factory method
|
|
let assistantMessage: Sampling.Message = .assistant("Hi there!")
|
|
#expect(assistantMessage.role == .assistant)
|
|
if case .single(.text(let text)) = assistantMessage.content {
|
|
#expect(text == "Hi there!")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test with image content
|
|
let imageMessage: Sampling.Message = .user(
|
|
.image(data: "base64data", mimeType: "image/png"))
|
|
#expect(imageMessage.role == .user)
|
|
if case .single(.image(let data, let mimeType)) = imageMessage.content {
|
|
#expect(data == "base64data")
|
|
#expect(mimeType == "image/png")
|
|
} else {
|
|
#expect(Bool(false), "Expected image content")
|
|
}
|
|
}
|
|
|
|
@Test("Content ExpressibleByStringLiteral")
|
|
func testContentExpressibleByStringLiteral() throws {
|
|
// Test string literal assignment
|
|
let content: Sampling.Message.Content = "Hello from string literal"
|
|
|
|
if case .single(.text(let text)) = content {
|
|
#expect(text == "Hello from string literal")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test in message creation
|
|
let message: Sampling.Message = .user("Direct string literal")
|
|
if case .single(.text(let text)) = message.content {
|
|
#expect(text == "Direct string literal")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test in array context
|
|
let messages: [Sampling.Message] = [
|
|
.user("First message"),
|
|
.assistant("Second message"),
|
|
.user("Third message"),
|
|
]
|
|
|
|
#expect(messages.count == 3)
|
|
#expect(messages[0].role == .user)
|
|
#expect(messages[1].role == .assistant)
|
|
#expect(messages[2].role == .user)
|
|
}
|
|
|
|
@Test("Content ExpressibleByStringInterpolation")
|
|
func testContentExpressibleByStringInterpolation() throws {
|
|
let userName = "Alice"
|
|
let temperature = 72
|
|
let location = "San Francisco"
|
|
|
|
// Test string interpolation
|
|
let content: Sampling.Message.Content =
|
|
"Hello \(userName), the temperature in \(location) is \(temperature)°F"
|
|
|
|
if case .single(.text(let text)) = content {
|
|
#expect(text == "Hello Alice, the temperature in San Francisco is 72°F")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test in message creation with interpolation
|
|
let message = Sampling.Message.user(
|
|
"Welcome \(userName)! Today's weather in \(location) is \(temperature)°F")
|
|
if case .single(.text(let text)) = message.content {
|
|
#expect(text == "Welcome Alice! Today's weather in San Francisco is 72°F")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test complex interpolation
|
|
let items = ["apples", "bananas", "oranges"]
|
|
let count = items.count
|
|
let listMessage: Sampling.Message = .assistant(
|
|
"You have \(count) items: \(items.joined(separator: ", "))")
|
|
|
|
if case .single(.text(let text)) = listMessage.content {
|
|
#expect(text == "You have 3 items: apples, bananas, oranges")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
}
|
|
|
|
@Test("Message factory methods with string interpolation")
|
|
func testMessageFactoryMethodsWithStringInterpolation() throws {
|
|
let customerName = "Bob"
|
|
let orderNumber = "ORD-12345"
|
|
let issueType = "delivery delay"
|
|
|
|
// Test user message with interpolation
|
|
let userMessage: Sampling.Message = .user(
|
|
"Hi, I'm \(customerName) and I have an issue with order \(orderNumber)")
|
|
#expect(userMessage.role == .user)
|
|
if case .single(.text(let text)) = userMessage.content {
|
|
#expect(text == "Hi, I'm Bob and I have an issue with order ORD-12345")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test assistant message with interpolation
|
|
let assistantMessage: Sampling.Message = .assistant(
|
|
"Hello \(customerName), I can help you with your \(issueType) issue for order \(orderNumber)"
|
|
)
|
|
#expect(assistantMessage.role == .assistant)
|
|
if case .single(.text(let text)) = assistantMessage.content {
|
|
#expect(
|
|
text
|
|
== "Hello Bob, I can help you with your delivery delay issue for order ORD-12345"
|
|
)
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
|
|
// Test in conversation array
|
|
let conversation: [Sampling.Message] = [
|
|
.user("Hello, I'm \(customerName)"),
|
|
.assistant("Hi \(customerName), how can I help you today?"),
|
|
.user("I have an issue with order \(orderNumber) - it's a \(issueType)"),
|
|
.assistant(
|
|
"I understand you're experiencing a \(issueType) with order \(orderNumber). Let me look into that for you."
|
|
),
|
|
]
|
|
|
|
#expect(conversation.count == 4)
|
|
|
|
// Verify interpolated content
|
|
if case .single(.text(let text)) = conversation[2].content {
|
|
#expect(text == "I have an issue with order ORD-12345 - it's a delivery delay")
|
|
} else {
|
|
#expect(Bool(false), "Expected text content")
|
|
}
|
|
}
|
|
|
|
@Test("Ergonomic API usage patterns")
|
|
func testErgonomicAPIUsagePatterns() throws {
|
|
// Test various ergonomic usage patterns enabled by the new API
|
|
|
|
// Pattern 1: Simple conversation
|
|
let simpleConversation: [Sampling.Message] = [
|
|
.user("What's the weather like?"),
|
|
.assistant("I'd be happy to help you check the weather!"),
|
|
.user("Thanks!"),
|
|
]
|
|
#expect(simpleConversation.count == 3)
|
|
|
|
// Pattern 2: Dynamic content with interpolation
|
|
let productName = "Smart Thermostat"
|
|
let price = 199.99
|
|
let discount = 20
|
|
|
|
let salesConversation: [Sampling.Message] = [
|
|
.user("Tell me about the \(productName)"),
|
|
.assistant("The \(productName) is priced at $\(String(format: "%.2f", price))"),
|
|
.user("Do you have any discounts?"),
|
|
.assistant(
|
|
"Yes! We currently have a \(discount)% discount, bringing the price to $\(String(format: "%.2f", price * (1.0 - Double(discount)/100.0)))"
|
|
),
|
|
]
|
|
#expect(salesConversation.count == 4)
|
|
|
|
// Pattern 3: Mixed content types
|
|
let mixedContent: [Sampling.Message] = [
|
|
.user("Can you analyze this image?"),
|
|
.assistant(.image(data: "analysis_chart_data", mimeType: "image/png")),
|
|
.user("What does it show?"),
|
|
.assistant("The chart shows a clear upward trend in sales."),
|
|
]
|
|
#expect(mixedContent.count == 4)
|
|
|
|
// Verify content types
|
|
if case .single(.text) = mixedContent[0].content,
|
|
case .single(.image) = mixedContent[1].content,
|
|
case .single(.text) = mixedContent[2].content,
|
|
case .single(.text) = mixedContent[3].content
|
|
{
|
|
// All content types are correct
|
|
} else {
|
|
#expect(Bool(false), "Content types don't match expected pattern")
|
|
}
|
|
|
|
// Pattern 4: Encoding/decoding still works
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let data = try encoder.encode(simpleConversation)
|
|
let decoded = try decoder.decode([Sampling.Message].self, from: data)
|
|
|
|
#expect(decoded.count == 3)
|
|
#expect(decoded[0].role == .user)
|
|
#expect(decoded[1].role == .assistant)
|
|
#expect(decoded[2].role == .user)
|
|
}
|
|
}
|
|
|
|
@Suite("Sampling Integration Tests")
|
|
struct SamplingIntegrationTests {
|
|
|
|
@Test
|
|
func testSamplingHandlerRegistration() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
|
|
let server = Server(
|
|
name: "SamplingHandlerTestServer",
|
|
version: "1.0"
|
|
)
|
|
|
|
let client = Client(
|
|
name: "SamplingHandlerTestClient",
|
|
version: "1.0"
|
|
)
|
|
|
|
nonisolated(unsafe) var handlerCalled = false
|
|
|
|
// Register sampling handler
|
|
await client.withSamplingHandler { parameters in
|
|
handlerCalled = true
|
|
#expect(parameters.messages.count == 1)
|
|
|
|
// Mock LLM response
|
|
return CreateSamplingMessage.Result(
|
|
model: "test-model-v1",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text("This is a test completion from the mock LLM.")
|
|
)
|
|
}
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
// Test that the handler actually gets called
|
|
let messages: [Sampling.Message] = [.user("Test")]
|
|
let result = try await server.requestSampling(messages: messages, maxTokens: 100)
|
|
|
|
#expect(handlerCalled, "Sampling handler should have been called")
|
|
#expect(result.model == "test-model-v1")
|
|
#expect(result.stopReason == .endTurn)
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
}
|
|
|
|
@Test
|
|
func testServerSamplingRequestAPI() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
|
|
let server = Server(
|
|
name: "SamplingRequestTestServer",
|
|
version: "1.0"
|
|
)
|
|
|
|
let client = Client(
|
|
name: "SamplingTestClient",
|
|
version: "1.0"
|
|
)
|
|
|
|
// Register sampling handler on client to respond to server's request
|
|
let responseContentString = "Based on the analysis, sales show strong growth through Q3 with Q4 stabilization."
|
|
await client.withSamplingHandler { parameters in
|
|
// Verify the request parameters were passed correctly
|
|
#expect(parameters.messages.count == 3)
|
|
#expect(parameters.systemPrompt == "You are a business analyst expert.")
|
|
#expect(parameters.includeContext == .thisServer)
|
|
#expect(parameters.temperature == 0.7)
|
|
#expect(parameters.maxTokens == 500)
|
|
#expect(parameters.stopSequences?.count == 2)
|
|
#expect(parameters._meta?["requestId"]?.stringValue == "test-123")
|
|
|
|
// Return mock LLM response
|
|
return CreateSamplingMessage.Result(
|
|
model: "claude-4-sonnet",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text(responseContentString)
|
|
)
|
|
}
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
// Test sampling request with comprehensive parameters
|
|
let messages: [Sampling.Message] = [
|
|
.user("Analyze the following data and provide insights:"),
|
|
.user("Sales data: Q1: $100k, Q2: $150k, Q3: $200k, Q4: $180k"),
|
|
.user("Marketing data: Q1: $50k, Q2: $75k, Q3: $100k, Q4: $90k"),
|
|
]
|
|
|
|
let modelPreferences = Sampling.ModelPreferences(
|
|
hints: [
|
|
Sampling.ModelPreferences.Hint(name: "claude-4-sonnet"),
|
|
Sampling.ModelPreferences.Hint(name: "gpt-4.1"),
|
|
],
|
|
costPriority: 0.3,
|
|
speedPriority: 0.7,
|
|
intelligencePriority: 0.9
|
|
)
|
|
|
|
// Test that the API works end-to-end
|
|
let result = try await server.requestSampling(
|
|
messages: messages,
|
|
modelPreferences: modelPreferences,
|
|
systemPrompt: "You are a business analyst expert.",
|
|
includeContext: .thisServer,
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
stopSequences: ["END_ANALYSIS", "\n\n---"],
|
|
_meta: Metadata(additionalFields: [
|
|
"requestId": "test-123",
|
|
"priority": "high",
|
|
"department": "analytics",
|
|
])
|
|
)
|
|
|
|
// Verify the response
|
|
#expect(result.model == "claude-4-sonnet")
|
|
#expect(result.stopReason == .endTurn)
|
|
#expect(result.role == .assistant)
|
|
if case .single(.text(let text)) = result.content {
|
|
#expect(text == responseContentString)
|
|
} else {
|
|
Issue.record("Expected text content")
|
|
}
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
}
|
|
|
|
@Test
|
|
func testSamplingMessageTypes() async throws {
|
|
// Test comprehensive message content types
|
|
let textMessage: Sampling.Message = .user("What do you see in this data?")
|
|
|
|
let imageMessage: Sampling.Message = .user(
|
|
.image(
|
|
data:
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
|
mimeType: "image/png"
|
|
))
|
|
|
|
// Test encoding/decoding of different message types
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
// Test text message
|
|
let textData = try encoder.encode(textMessage)
|
|
let decodedTextMessage = try decoder.decode(Sampling.Message.self, from: textData)
|
|
#expect(decodedTextMessage == textMessage)
|
|
|
|
// Test image message
|
|
let imageData = try encoder.encode(imageMessage)
|
|
let decodedImageMessage = try decoder.decode(Sampling.Message.self, from: imageData)
|
|
#expect(decodedImageMessage == imageMessage)
|
|
}
|
|
|
|
@Test
|
|
func testSamplingResultTypes() async throws {
|
|
// Test different result content types and stop reasons
|
|
let textResult = CreateSamplingMessage.Result(
|
|
model: "claude-4-sonnet",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text(
|
|
"Based on the sales data analysis, I can see a strong upward trend through Q3, with a slight decline in Q4. This suggests seasonal factors or market saturation."
|
|
)
|
|
)
|
|
|
|
let imageResult = CreateSamplingMessage.Result(
|
|
model: "dall-e-3",
|
|
stopReason: .maxTokens,
|
|
role: .assistant,
|
|
content: .image(
|
|
data: "generated_chart_base64_data_here",
|
|
mimeType: "image/png"
|
|
)
|
|
)
|
|
|
|
let stopSequenceResult = CreateSamplingMessage.Result(
|
|
model: "gpt-4.1",
|
|
stopReason: .stopSequence,
|
|
role: .assistant,
|
|
content: .text("Analysis complete.\nEND_ANALYSIS")
|
|
)
|
|
|
|
// Test encoding/decoding of different result types
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
// Test text result
|
|
let textData = try encoder.encode(textResult)
|
|
let decodedTextResult = try decoder.decode(
|
|
CreateSamplingMessage.Result.self, from: textData
|
|
)
|
|
#expect(decodedTextResult == textResult)
|
|
|
|
// Test image result
|
|
let imageData = try encoder.encode(imageResult)
|
|
let decodedImageResult = try decoder.decode(
|
|
CreateSamplingMessage.Result.self, from: imageData
|
|
)
|
|
#expect(decodedImageResult == imageResult)
|
|
|
|
// Test stop sequence result
|
|
let stopData = try encoder.encode(stopSequenceResult)
|
|
let decodedStopResult = try decoder.decode(
|
|
CreateSamplingMessage.Result.self, from: stopData
|
|
)
|
|
#expect(decodedStopResult == stopSequenceResult)
|
|
}
|
|
|
|
@Test
|
|
func testSamplingErrorHandling() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
|
|
let server = Server(
|
|
name: "ErrorTestServer",
|
|
version: "1.0"
|
|
)
|
|
|
|
// Client WITHOUT sampling capability
|
|
let client = Client(
|
|
name: "ErrorTestClient",
|
|
version: "1.0"
|
|
)
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
// Test sampling request - should fail because client doesn't support sampling
|
|
let messages: [Sampling.Message] = [
|
|
.user("Test message")
|
|
]
|
|
|
|
await #expect(throws: MCPError.self) {
|
|
_ = try await server.requestSampling(
|
|
messages: messages,
|
|
maxTokens: 100
|
|
)
|
|
}
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
}
|
|
|
|
@Test("Strict mode succeeds when client declares sampling capability")
|
|
func testSamplingStrictCapabilitiesSuccess() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
|
|
let server = Server(
|
|
name: "StrictTestServer",
|
|
version: "1.0",
|
|
configuration: .strict
|
|
)
|
|
|
|
let client = Client(
|
|
name: "StrictTestClient",
|
|
version: "1.0",
|
|
capabilities: .init(sampling: .init()),
|
|
configuration: .strict
|
|
)
|
|
|
|
// Register sampling handler
|
|
await client.withSamplingHandler { _ in
|
|
CreateSamplingMessage.Result(
|
|
model: "test-model",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text("Strict mode success")
|
|
)
|
|
}
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
// Should succeed because client declares sampling capability
|
|
let result = try await server.requestSampling(
|
|
messages: [.user("Test message")],
|
|
maxTokens: 100
|
|
)
|
|
|
|
#expect(result.model == "test-model")
|
|
#expect(result.role == .assistant)
|
|
if case .single(.text(let text)) = result.content {
|
|
#expect(text == "Strict mode success")
|
|
} else {
|
|
Issue.record("Expected text content")
|
|
}
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
}
|
|
|
|
@Test("Strict mode fails when client doesn't declare sampling capability")
|
|
func testSamplingStrictCapabilitiesError() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
|
|
let server = Server(
|
|
name: "StrictTestServer",
|
|
version: "1.0",
|
|
configuration: .strict
|
|
)
|
|
|
|
// Client WITHOUT sampling capability in strict mode
|
|
let client = Client(
|
|
name: "StrictTestClient",
|
|
version: "1.0",
|
|
capabilities: .init(),
|
|
configuration: .strict
|
|
)
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
// Should fail because client doesn't declare sampling capability in strict mode
|
|
await #expect(throws: MCPError.self) {
|
|
_ = try await server.requestSampling(
|
|
messages: [.user("Test message")],
|
|
maxTokens: 100
|
|
)
|
|
}
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
}
|
|
|
|
@Test("Non-strict mode succeeds even without client capability declaration")
|
|
func testSamplingNonStrictCapabilities() async throws {
|
|
let (clientTransport, serverTransport) = await InMemoryTransport.createConnectedPair()
|
|
|
|
let server = Server(
|
|
name: "NonStrictTestServer",
|
|
version: "1.0",
|
|
configuration: .default // Non-strict mode
|
|
)
|
|
|
|
// Client WITHOUT sampling capability in non-strict mode
|
|
let client = Client(
|
|
name: "NonStrictTestClient",
|
|
version: "1.0",
|
|
capabilities: .init(),
|
|
configuration: .default
|
|
)
|
|
|
|
// Register sampling handler anyway
|
|
await client.withSamplingHandler { _ in
|
|
CreateSamplingMessage.Result(
|
|
model: "test-model",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text("Non-strict mode success")
|
|
)
|
|
}
|
|
|
|
try await server.start(transport: serverTransport)
|
|
try await client.connect(transport: clientTransport)
|
|
|
|
// Should succeed because server is in non-strict mode
|
|
let result = try await server.requestSampling(
|
|
messages: [.user("Test message")],
|
|
maxTokens: 100
|
|
)
|
|
|
|
#expect(result.model == "test-model")
|
|
if case .single(.text(let text)) = result.content {
|
|
#expect(text == "Non-strict mode success")
|
|
} else {
|
|
Issue.record("Expected text content")
|
|
}
|
|
|
|
await server.stop()
|
|
await client.disconnect()
|
|
}
|
|
|
|
@Test
|
|
func testSamplingParameterValidation() async throws {
|
|
// Test parameter validation and edge cases
|
|
let validMessages: [Sampling.Message] = [
|
|
.user("Valid message")
|
|
]
|
|
|
|
// Test with valid parameters
|
|
let validParams = CreateSamplingMessage.Parameters(
|
|
messages: validMessages,
|
|
maxTokens: 100
|
|
)
|
|
#expect(validParams.messages.count == 1)
|
|
#expect(validParams.maxTokens == 100)
|
|
|
|
// Test with comprehensive parameters
|
|
let comprehensiveParams = CreateSamplingMessage.Parameters(
|
|
messages: validMessages,
|
|
modelPreferences: Sampling.ModelPreferences(
|
|
hints: [Sampling.ModelPreferences.Hint(name: "claude-4")],
|
|
costPriority: 0.5,
|
|
speedPriority: 0.8,
|
|
intelligencePriority: 0.9
|
|
),
|
|
systemPrompt: "You are a helpful assistant.",
|
|
includeContext: .allServers,
|
|
temperature: 0.7,
|
|
maxTokens: 500,
|
|
stopSequences: ["STOP", "END"],
|
|
_meta: Metadata(additionalFields: [
|
|
"sessionId": "test-session-123",
|
|
"userId": "user-456",
|
|
])
|
|
)
|
|
|
|
#expect(comprehensiveParams.messages.count == 1)
|
|
#expect(comprehensiveParams.modelPreferences?.hints?.count == 1)
|
|
#expect(comprehensiveParams.systemPrompt == "You are a helpful assistant.")
|
|
#expect(comprehensiveParams.includeContext == .allServers)
|
|
#expect(comprehensiveParams.temperature == 0.7)
|
|
#expect(comprehensiveParams.maxTokens == 500)
|
|
#expect(comprehensiveParams.stopSequences?.count == 2)
|
|
#expect(comprehensiveParams._meta?.fields.count == 2)
|
|
|
|
// Test encoding/decoding of comprehensive parameters
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let data = try encoder.encode(comprehensiveParams)
|
|
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
|
|
|
|
#expect(decoded == comprehensiveParams)
|
|
}
|
|
|
|
@Test
|
|
func testSamplingWorkflowScenarios() async throws {
|
|
// Test realistic sampling workflow scenarios
|
|
|
|
// Scenario 1: Data Analysis Request
|
|
let dataAnalysisMessages: [Sampling.Message] = [
|
|
.user("Please analyze the following customer feedback data:"),
|
|
.user(
|
|
"""
|
|
Feedback Summary:
|
|
- 85% positive sentiment
|
|
- Top complaints: shipping delays (12%), product quality (8%)
|
|
- Top praise: customer service (45%), product features (40%)
|
|
- NPS Score: 72
|
|
"""),
|
|
]
|
|
|
|
let dataAnalysisParams = CreateSamplingMessage.Parameters(
|
|
messages: dataAnalysisMessages,
|
|
modelPreferences: Sampling.ModelPreferences(
|
|
hints: [Sampling.ModelPreferences.Hint(name: "claude-4-sonnet")],
|
|
speedPriority: 0.3,
|
|
intelligencePriority: 0.9
|
|
),
|
|
systemPrompt: "You are an expert business analyst. Provide actionable insights.",
|
|
includeContext: .thisServer,
|
|
temperature: 0.3, // Lower temperature for analytical tasks
|
|
maxTokens: 400,
|
|
stopSequences: ["---END---"],
|
|
_meta: Metadata(additionalFields: ["analysisType": "customer-feedback"])
|
|
)
|
|
|
|
// Scenario 2: Creative Content Generation
|
|
let creativeMessages: [Sampling.Message] = [
|
|
.user(
|
|
"Write a compelling product description for a new smart home device.")
|
|
]
|
|
|
|
let creativeParams = CreateSamplingMessage.Parameters(
|
|
messages: creativeMessages,
|
|
modelPreferences: Sampling.ModelPreferences(
|
|
hints: [Sampling.ModelPreferences.Hint(name: "gpt-4.1")],
|
|
costPriority: 0.4,
|
|
speedPriority: 0.6,
|
|
intelligencePriority: 0.8
|
|
),
|
|
systemPrompt: "You are a creative marketing copywriter.",
|
|
temperature: 0.8, // Higher temperature for creativity
|
|
maxTokens: 200,
|
|
_meta: Metadata(additionalFields: ["contentType": "marketing-copy"])
|
|
)
|
|
|
|
// Test parameter encoding for both scenarios
|
|
let encoder = JSONEncoder()
|
|
|
|
let analysisData = try encoder.encode(dataAnalysisParams)
|
|
let creativeData = try encoder.encode(creativeParams)
|
|
|
|
// Test decoding
|
|
let decoder = JSONDecoder()
|
|
let decodedAnalysis = try decoder.decode(
|
|
CreateSamplingMessage.Parameters.self, from: analysisData)
|
|
let decodedCreative = try decoder.decode(
|
|
CreateSamplingMessage.Parameters.self, from: creativeData)
|
|
|
|
#expect(decodedAnalysis == dataAnalysisParams)
|
|
#expect(decodedCreative == creativeParams)
|
|
}
|
|
}
|
|
|
|
@Suite("Sampling 2025-11-25 Spec Tests")
|
|
struct Sampling2025_11_25Tests {
|
|
@Test("Audio content encoding and decoding")
|
|
func testAudioContent() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let audioMessage: Sampling.Message = .user(
|
|
.audio(data: "base64audiodata", mimeType: "audio/mp3"))
|
|
|
|
let data = try encoder.encode(audioMessage)
|
|
let decoded = try decoder.decode(Sampling.Message.self, from: data)
|
|
|
|
#expect(decoded.role == .user)
|
|
if case .single(.audio(let audioData, let mimeType)) = decoded.content {
|
|
#expect(audioData == "base64audiodata")
|
|
#expect(mimeType == "audio/mp3")
|
|
} else {
|
|
#expect(Bool(false), "Expected audio content")
|
|
}
|
|
}
|
|
|
|
@Test("StopReason.toolUse encoding and decoding")
|
|
func testToolUseStopReason() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let result = CreateSamplingMessage.Result(
|
|
model: "claude-4",
|
|
stopReason: .toolUse,
|
|
role: .assistant,
|
|
content: .single(.text("I need to use a tool"))
|
|
)
|
|
|
|
let data = try encoder.encode(result)
|
|
let decoded = try decoder.decode(CreateSamplingMessage.Result.self, from: data)
|
|
|
|
#expect(decoded == result)
|
|
}
|
|
|
|
@Test("Multiple content blocks")
|
|
func testMultipleContentBlocks() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let blocks: [Sampling.Message.Content.ContentBlock] = [
|
|
.text("Here's an image:"),
|
|
.image(data: "imagedata", mimeType: "image/png"),
|
|
.text("And some audio:"),
|
|
.audio(data: "audiodata", mimeType: "audio/mp3")
|
|
]
|
|
|
|
let content = Sampling.Message.Content.multiple(blocks)
|
|
|
|
let message = Sampling.Message.assistant(content)
|
|
let data = try encoder.encode(message)
|
|
let decoded = try decoder.decode(Sampling.Message.self, from: data)
|
|
|
|
#expect(decoded == message)
|
|
}
|
|
|
|
@Test("Tools parameter encoding and decoding")
|
|
func testToolsParameter() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let tool = Tool(
|
|
name: "get_weather",
|
|
description: "Get current weather",
|
|
inputSchema: [
|
|
"type": "object",
|
|
"properties": [
|
|
"location": ["type": "string"]
|
|
]
|
|
]
|
|
)
|
|
|
|
let params = CreateSamplingMessage.Parameters(
|
|
messages: [.user("What's the weather?")],
|
|
maxTokens: 100,
|
|
tools: [tool],
|
|
toolChoice: .init(mode: .auto)
|
|
)
|
|
|
|
let data = try encoder.encode(params)
|
|
let decoded = try decoder.decode(CreateSamplingMessage.Parameters.self, from: data)
|
|
|
|
#expect(decoded == params)
|
|
}
|
|
|
|
@Test("Client sampling capabilities with sub-capabilities")
|
|
func testSamplingSubCapabilities() throws {
|
|
let encoder = JSONEncoder()
|
|
let decoder = JSONDecoder()
|
|
|
|
let capabilities = Client.Capabilities(
|
|
sampling: .init(tools: .init(), context: .init())
|
|
)
|
|
|
|
#expect(capabilities.sampling?.tools != nil)
|
|
#expect(capabilities.sampling?.context != nil)
|
|
|
|
let data = try encoder.encode(capabilities)
|
|
let decoded = try decoder.decode(Client.Capabilities.self, from: data)
|
|
|
|
#expect(decoded == capabilities)
|
|
}
|
|
}
|