feat: 2025-11-25 review fixes (#201)

* 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
This commit is contained in:
yehorsobko 2026-03-03 16:33:28 +01:00 • committed by GitHub
commit 973b46b8f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 692 additions and 507 deletions

View file

@ -133,7 +133,7 @@ struct CancellationTests {
}
await server.withMethodHandler(CallTool.self) { params in
return .init(content: [.text("Result for \(params.name)")], isError: false)
return .init(content: [.text(text: "Result for \(params.name)", annotations: nil, _meta: nil)], isError: false)
}
// Start server and connect client
@ -149,7 +149,7 @@ struct CancellationTests {
// Get the result
let result = try await context.value
#expect(result.content == [.text("Result for testTool")])
#expect(result.content == [.text(text: "Result for testTool", annotations: nil, _meta: nil)])
#expect(result.isError == false)
await client.disconnect()
@ -174,7 +174,7 @@ struct CancellationTests {
await server.withMethodHandler(CallTool.self) { params in
try await Task.sleep(for: .seconds(5))
return .init(content: [.text("Should not reach here")], isError: false)
return .init(content: [.text(text: "Should not reach here", annotations: nil, _meta: nil)], isError: false)
}
// Start server and connect client

View file

@ -128,12 +128,12 @@ struct CompletionTests {
func testCompleteRequestWithContext() throws {
let ref = CompletionReference.prompt(PromptReference(name: "code_review"))
let argument = Complete.Parameters.Argument(name: "framework", value: "fla")
let context = Complete.Parameters.Context(arguments: ["language": .string("python")])
let context = Complete.Parameters.Context(arguments: ["language": "python"])
let request = Complete.request(.init(ref: ref, argument: argument, context: context))
#expect(request.params.context != nil)
#expect(request.params.context?.arguments["language"] == .string("python"))
#expect(request.params.context?.arguments["language"] == "python")
}
@Test("Complete request encoding")
@ -396,7 +396,7 @@ struct CompletionTests {
#expect(params.argument.name == "framework")
#expect(params.argument.value == "fla")
#expect(params.context != nil)
#expect(params.context?.arguments["language"] == .string("python"))
#expect(params.context?.arguments["language"] == "python")
return .init(
completion: .init(
@ -415,7 +415,7 @@ struct CompletionTests {
promptName: "code_review",
argumentName: "framework",
argumentValue: "fla",
context: ["language": .string("python")]
context: ["language": "python"]
)
#expect(completion.values == ["flask"])

View file

@ -176,7 +176,7 @@ struct Elicitation2025_11_25Tests {
let decoder = JSONDecoder()
let params = CreateElicitation.Parameters.form(
.init(message: "Enter your name")
.init(message: "Enter your name", requestedSchema: .init())
)
let data = try encoder.encode(params)
@ -262,7 +262,7 @@ struct ElicitationIntegrationTests {
await client.withElicitationHandler { parameters in
if case .form(let formParams) = parameters {
#expect(formParams.message == "Please enter your details")
#expect(formParams.requestedSchema?.properties["email"] != nil)
#expect(formParams.requestedSchema.properties["email"] != nil)
#expect(formParams._meta?["flow"]?.stringValue == "onboarding")
// Return accepted response
@ -384,7 +384,8 @@ struct ElicitationIntegrationTests {
try await client.connect(transport: clientTransport)
let result = try await server.requestElicitation(
message: "Optional question"
message: "Optional question",
requestedSchema: .init()
)
#expect(result.action == .decline)
@ -414,7 +415,8 @@ struct ElicitationIntegrationTests {
// Should throw an error because client doesn't have elicitation capability
await #expect(throws: MCPError.self) {
_ = try await server.requestElicitation(
message: "Test message"
message: "Test message",
requestedSchema: .init()
)
}
@ -452,7 +454,8 @@ struct ElicitationIntegrationTests {
// Should succeed because client declares elicitation capability
let result = try await server.requestElicitation(
message: "Test message"
message: "Test message",
requestedSchema: .init()
)
#expect(result.action == .accept)
@ -486,7 +489,8 @@ struct ElicitationIntegrationTests {
// Should fail because client doesn't declare elicitation capability in strict mode
await #expect(throws: MCPError.self) {
_ = try await server.requestElicitation(
message: "Test message"
message: "Test message",
requestedSchema: .init()
)
}
@ -525,7 +529,8 @@ struct ElicitationIntegrationTests {
// Should succeed because server is in non-strict mode
let result = try await server.requestElicitation(
message: "Test message"
message: "Test message",
requestedSchema: .init()
)
#expect(result.action == .accept)
@ -608,15 +613,15 @@ struct ElicitationIntegrationTests {
try await client.connect(transport: clientTransport)
// Make multiple sequential requests
let result1 = try await server.requestElicitation(message: "First question")
let result1 = try await server.requestElicitation(message: "First question", requestedSchema: .init())
#expect(result1.action == .accept)
#expect(result1.content?["echo"]?.stringValue == "First question")
let result2 = try await server.requestElicitation(message: "Second question")
let result2 = try await server.requestElicitation(message: "Second question", requestedSchema: .init())
#expect(result2.action == .accept)
#expect(result2.content?["echo"]?.stringValue == "Second question")
let result3 = try await server.requestElicitation(message: "Third question")
let result3 = try await server.requestElicitation(message: "Third question", requestedSchema: .init())
#expect(result3.action == .accept)
#expect(result3.content?["echo"]?.stringValue == "Third question")

View file

@ -637,7 +637,7 @@ import Testing
#expect(toolName == "calculator")
let requestID = json["id"] as! String
let result = CallTool.Result(content: [.text("42")])
let result = CallTool.Result(content: [.text(text: "42", annotations: nil, _meta: nil)])
let response = CallTool.response(id: .string(requestID), result: result)
let responseData = try JSONEncoder().encode(response)
@ -669,7 +669,7 @@ import Testing
// Step 2: Call a tool
let toolResult = try await client.callTool(name: "calculator")
#expect(toolResult.content.count == 1)
if case let .text(text) = toolResult.content[0] {
if case let .text(text, _, _) = toolResult.content[0] {
#expect(text == "42")
} else {
#expect(Bool(false), "Expected text content")

View file

@ -18,60 +18,6 @@ struct MetaFieldsTests {
typealias Result = Payload
}
@Test("Encoding includes meta and custom fields")
func testEncodingGeneralFields() throws {
let meta: Metadata = Metadata(additionalFields: ["vendor.example/request-id": .string("abc123")])
let request = Request<TestMethod>(
id: 42,
method: TestMethod.name,
params: Payload(message: "hello"),
_meta: meta
)
let data = try JSONEncoder().encode(request)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let metaObject = json?["_meta"] as? [String: Any]
#expect(metaObject?["vendor.example/request-id"] as? String == "abc123")
}
@Test("Decoding restores general fields")
func testDecodingGeneralFields() throws {
let payload: [String: Any] = [
"jsonrpc": "2.0",
"id": 7,
"method": TestMethod.name,
"params": ["message": "hi"],
"_meta": ["vendor.example/session": "s42"],
"custom-data": ["value": 1],
]
let data = try JSONSerialization.data(withJSONObject: payload)
let decoded = try JSONDecoder().decode(Request<TestMethod>.self, from: data)
let metaValue = decoded._meta?["vendor.example/session"]
#expect(metaValue == .string("s42"))
}
@Test("Response encoding includes general fields")
func testResponseGeneralFields() throws {
let meta = Metadata(additionalFields: ["vendor.example/status": .string("partial")])
let response = Response<TestMethod>(
id: 99,
result: .success(Payload(message: "ok")),
_meta: meta
)
let data = try JSONEncoder().encode(response)
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
let metaObject = json?["_meta"] as? [String: Any]
#expect(metaObject?["vendor.example/status"] as? String == "partial")
let decoded = try JSONDecoder().decode(Response<TestMethod>.self, from: data)
#expect(decoded._meta?["vendor.example/status"] == .string("partial"))
}
@Test("Tool encoding and decoding with general fields")
func testToolGeneralFields() throws {
let meta = Metadata(additionalFields: [
@ -199,7 +145,7 @@ struct MetaFieldsTests {
let meta = Metadata(additionalFields: ["vendor.example/executionTime": .int(150)])
let result = CallTool.Result(
content: [.text("Result data")],
content: [.text(text: "Result data", annotations: nil, _meta: nil)],
isError: false,
_meta: meta
)

View file

@ -364,7 +364,7 @@ struct ProgressTests {
}
let server = Server(name: "testServer", version: "1")
let expectedToolCallResult = CallTool.Result(content: [.text("success")])
let expectedToolCallResult = CallTool.Result(content: [.text(text: "success", annotations: nil, _meta: nil)])
await server.withMethodHandler(CallTool.self) { params in
if let token = params._meta?.progressToken {
for i in 1...5 {
@ -375,7 +375,7 @@ struct ProgressTests {
}
}
return .init(content: [.text("success")])
return .init(content: [.text(text: "success", annotations: nil, _meta: nil)])
}
try await server.start(transport: pair.server)

View file

@ -125,9 +125,9 @@ struct PromptTests {
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let arguments: [String: Value] = [
"param1": .string("value1"),
"param2": .int(42),
let arguments: [String: String] = [
"param1": "value1",
"param2": "42",
]
let params = GetPrompt.Parameters(name: "test_prompt", arguments: arguments)
@ -570,14 +570,14 @@ struct PromptIntegrationTests {
)
let expectedMessages: [Prompt.Message] = [
.user("Hello, I'd like to schedule an interview for the \(Value.string("Software Engineer")) position"),
.user("Hello, I'd like to schedule an interview for the Software Engineer position"),
.assistant("I'd be happy to help you prepare for the Software Engineer interview. Let's discuss your background."),
]
// Register get prompt handler
await server.withMethodHandler(GetPrompt.self) { params in
#expect(params.name == "interview")
#expect(params.arguments?["position"]?.stringValue == "Software Engineer")
#expect(params.arguments?["position"] == "Software Engineer")
return GetPrompt.Result(
description: "Interview preparation prompt",
@ -595,7 +595,7 @@ struct PromptIntegrationTests {
let (description, messages) = try await client.getPrompt(
name: "interview",
arguments: ["position": .string("Software Engineer")]
arguments: ["position": "Software Engineer"]
)
#expect(description == "Interview preparation prompt")

View file

@ -13,7 +13,7 @@ struct ResourceTests {
title: "Test Resource Title",
description: "A test resource",
mimeType: "text/plain",
metadata: ["key": "value"]
size: 1024
)
#expect(resource.name == "test_resource")
@ -21,7 +21,7 @@ struct ResourceTests {
#expect(resource.title == "Test Resource Title")
#expect(resource.description == "A test resource")
#expect(resource.mimeType == "text/plain")
#expect(resource.metadata?["key"] == "value")
#expect(resource.size == 1024)
}
@Test("Resource encoding and decoding")
@ -32,7 +32,7 @@ struct ResourceTests {
title: "Test Resource Title",
description: "Test resource description",
mimeType: "text/plain",
metadata: ["key1": "value1", "key2": "value2"]
size: 512
)
let encoder = JSONEncoder()
@ -46,7 +46,7 @@ struct ResourceTests {
#expect(decoded.title == resource.title)
#expect(decoded.description == resource.description)
#expect(decoded.mimeType == resource.mimeType)
#expect(decoded.metadata == resource.metadata)
#expect(decoded.size == resource.size)
}
@Test("Resource.Content text initialization and encoding")

View file

@ -57,17 +57,17 @@ struct RoundtripTests {
}
await server.withMethodHandler(CallTool.self) { request in
guard request.name == "add" else {
return CallTool.Result(content: [.text("Invalid tool name")], isError: true)
return CallTool.Result(content: [.text(text: "Invalid tool name", annotations: nil, _meta: nil)], isError: true)
}
guard let a = request.arguments?["a"]?.intValue,
let b = request.arguments?["b"]?.intValue
else {
return CallTool.Result(
content: [.text("Did not receive valid arguments")], isError: true)
content: [.text(text: "Did not receive valid arguments", annotations: nil, _meta: nil)], isError: true)
}
return CallTool.Result(content: [.text("\(a + b)")])
return CallTool.Result(content: [.text(text: "\(a + b)", annotations: nil, _meta: nil)])
}
// Add resource handlers to server
@ -151,7 +151,7 @@ struct RoundtripTests {
let callToolTask = Task {
let result = try await client.callTool(name: "add", arguments: ["a": 1, "b": 2])
#expect(result.isError == nil)
#expect(result.content == [.text("3")])
#expect(result.content == [.text(text: "3", annotations: nil, _meta: nil)])
}
try await withThrowingTaskGroup(of: Void.self) { group in

View file

@ -183,23 +183,6 @@ struct SamplingTests {
#expect(request.params.maxTokens == 100)
}
@Test("Server capabilities include sampling")
func testServerCapabilitiesIncludeSampling() throws {
let capabilities = Server.Capabilities(
sampling: .init()
)
#expect(capabilities.sampling != nil)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Server.Capabilities.self, from: data)
#expect(decoded.sampling != nil)
}
@Test("Client capabilities include sampling")
func testClientCapabilitiesIncludeSampling() throws {
let capabilities = Client.Capabilities(
@ -507,8 +490,7 @@ struct SamplingIntegrationTests {
let server = Server(
name: "SamplingHandlerTestServer",
version: "1.0",
capabilities: .init(sampling: .init())
version: "1.0"
)
let client = Client(
@ -553,8 +535,7 @@ struct SamplingIntegrationTests {
let server = Server(
name: "SamplingRequestTestServer",
version: "1.0",
capabilities: .init(sampling: .init())
version: "1.0"
)
let client = Client(
@ -721,8 +702,7 @@ struct SamplingIntegrationTests {
let server = Server(
name: "ErrorTestServer",
version: "1.0",
capabilities: .init(sampling: .init())
version: "1.0"
)
// Client WITHOUT sampling capability
@ -757,7 +737,6 @@ struct SamplingIntegrationTests {
let server = Server(
name: "StrictTestServer",
version: "1.0",
capabilities: .init(sampling: .init()),
configuration: .strict
)
@ -806,7 +785,6 @@ struct SamplingIntegrationTests {
let server = Server(
name: "StrictTestServer",
version: "1.0",
capabilities: .init(sampling: .init()),
configuration: .strict
)
@ -840,7 +818,6 @@ struct SamplingIntegrationTests {
let server = Server(
name: "NonStrictTestServer",
version: "1.0",
capabilities: .init(sampling: .init()),
configuration: .default // Non-strict mode
)

View file

@ -240,14 +240,14 @@ struct ToolTests {
@Test("Text content encoding and decoding")
func testToolContentTextEncoding() throws {
let content = Tool.Content.text("Hello, world!")
let content = Tool.Content.text(text: "Hello, world!", annotations: nil, _meta: nil)
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 {
if case .text(let text, _, _) = decoded {
#expect(text == "Hello, world!")
} else {
#expect(Bool(false), "Expected text content")
@ -259,7 +259,8 @@ struct ToolTests {
let content = Tool.Content.image(
data: "base64data",
mimeType: "image/png",
metadata: .init(additionalFields: ["width": "100", "height": "100"])
annotations: nil,
_meta: nil
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
@ -267,11 +268,9 @@ struct ToolTests {
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 {
if case .image(let data, let mimeType, _, _) = decoded {
#expect(data == "base64data")
#expect(mimeType == "image/png")
#expect(metadata?["width"] == "100")
#expect(metadata?["height"] == "100")
} else {
#expect(Bool(false), "Expected image content")
}
@ -337,7 +336,9 @@ struct ToolTests {
func testToolContentAudioEncoding() throws {
let content = Tool.Content.audio(
data: "base64audiodata",
mimeType: "audio/wav"
mimeType: "audio/wav",
annotations: nil,
_meta: nil
)
let encoder = JSONEncoder()
let decoder = JSONDecoder()
@ -345,7 +346,7 @@ struct ToolTests {
let data = try encoder.encode(content)
let decoded = try decoder.decode(Tool.Content.self, from: data)
if case .audio(let data, let mimeType) = decoded {
if case .audio(let data, let mimeType, _, _) = decoded {
#expect(data == "base64audiodata")
#expect(mimeType == "audio/wav")
} else {
@ -353,6 +354,71 @@ struct ToolTests {
}
}
@Test("Text content encoding includes annotations and meta fields")
func testToolContentTextEncodingIncludesAnnotationsAndMeta() throws {
let annotations = Resource.Annotations(
audience: [.assistant],
priority: 0.75,
lastModified: "2026-01-01T00:00:00Z"
)
let meta = Metadata(additionalFields: [
"vendor.example/source": .string("tool-text")
])
let expected = Tool.Content.text(
text: "Hello with metadata",
annotations: annotations,
_meta: meta
)
let data = try JSONEncoder().encode(expected)
let decoded = try JSONDecoder().decode(Tool.Content.self, from: data)
#expect(decoded == expected)
}
@Test("Image content encoding includes annotations and meta fields")
func testToolContentImageEncodingIncludesAnnotationsAndMeta() throws {
let annotations = Resource.Annotations(
audience: [.user],
priority: 0.9,
lastModified: "2026-01-02T00:00:00Z"
)
let meta = Metadata(additionalFields: [
"vendor.example/source": .string("tool-image")
])
let expected = Tool.Content.image(
data: "base64-image",
mimeType: "image/jpeg",
annotations: annotations,
_meta: meta
)
let data = try JSONEncoder().encode(expected)
let decoded = try JSONDecoder().decode(Tool.Content.self, from: data)
#expect(decoded == expected)
}
@Test("Audio content encoding includes annotations and meta fields")
func testToolContentAudioEncodingIncludesAnnotationsAndMeta() throws {
let annotations = Resource.Annotations(
audience: [.assistant],
priority: 0.4,
lastModified: "2026-01-03T00:00:00Z"
)
let meta = Metadata(additionalFields: [
"vendor.example/source": .string("tool-audio")
])
let expected = Tool.Content.audio(
data: "base64-audio",
mimeType: "audio/mp3",
annotations: annotations,
_meta: meta
)
let data = try JSONEncoder().encode(expected)
let decoded = try JSONDecoder().decode(Tool.Content.self, from: data)
#expect(decoded == expected)
}
@Test("ListTools parameters validation")
func testListToolsParameters() throws {
let params = ListTools.Parameters(cursor: "next_page")
@ -422,15 +488,15 @@ struct ToolTests {
@Test("CallTool success result validation")
func testCallToolResult() throws {
let content = [
Tool.Content.text("Result 1"),
Tool.Content.text("Result 2"),
Tool.Content.text(text: "Result 1", annotations: nil, _meta: nil),
Tool.Content.text(text: "Result 2", annotations: nil, _meta: nil),
]
let result = CallTool.Result(content: content)
#expect(result.content.count == 2)
#expect(result.isError == nil)
if case .text(let text) = result.content[0] {
if case .text(let text, _, _) = result.content[0] {
#expect(text == "Result 1")
} else {
#expect(Bool(false), "Expected text content")
@ -439,12 +505,12 @@ struct ToolTests {
@Test("CallTool error result validation")
func testCallToolErrorResult() throws {
let errorContent = [Tool.Content.text("Error message")]
let errorContent = [Tool.Content.text(text: "Error message", annotations: nil, _meta: nil)]
let errorResult = CallTool.Result(content: errorContent, isError: true)
#expect(errorResult.content.count == 1)
#expect(errorResult.isError == true)
if case .text(let text) = errorResult.content[0] {
if case .text(let text, _, _) = errorResult.content[0] {
#expect(text == "Error message")
} else {
#expect(Bool(false), "Expected error text content")