* 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>
351 lines
13 KiB
Swift
351 lines
13 KiB
Swift
import Testing
|
|
|
|
import class Foundation.JSONDecoder
|
|
import class Foundation.JSONEncoder
|
|
import class Foundation.JSONSerialization
|
|
|
|
@testable import MCP
|
|
|
|
@Suite("Meta Fields")
|
|
struct MetaFieldsTests {
|
|
private struct Payload: Codable, Hashable, Sendable {
|
|
let message: String
|
|
}
|
|
|
|
private enum TestMethod: Method {
|
|
static let name = "test.general"
|
|
typealias Parameters = Payload
|
|
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: [
|
|
"vendor.example/outputTemplate": .string("ui://widget/kanban-board.html")
|
|
])
|
|
|
|
let tool = Tool(
|
|
name: "kanban-board",
|
|
title: "Kanban Board",
|
|
description: "Display kanban widget",
|
|
inputSchema: try Value(["type": "object"]),
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(tool)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(
|
|
metaObject?["vendor.example/outputTemplate"] as? String
|
|
== "ui://widget/kanban-board.html")
|
|
|
|
let decoded = try JSONDecoder().decode(Tool.self, from: data)
|
|
#expect(
|
|
decoded._meta?["vendor.example/outputTemplate"]
|
|
== .string("ui://widget/kanban-board.html")
|
|
)
|
|
}
|
|
|
|
@Test("Meta keys allow nested prefixes")
|
|
func testMetaKeyNestedPrefixes() throws {
|
|
let meta = Metadata(additionalFields: [
|
|
"vendor.example/toolInvocation/invoking": .bool(true)
|
|
])
|
|
|
|
let tool = Tool(
|
|
name: "invoke",
|
|
description: "Invoke tool",
|
|
inputSchema: [:],
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(tool)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/toolInvocation/invoking"] as? Bool == true)
|
|
|
|
let decoded = try JSONDecoder().decode(Tool.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/toolInvocation/invoking"] == .bool(true))
|
|
}
|
|
|
|
@Test("Resource content encodes meta")
|
|
func testResourceContentGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: [
|
|
"vendor.example/widgetPrefersBorder": .bool(true)
|
|
])
|
|
|
|
let content = Resource.Content.text(
|
|
"<div>Widget</div>",
|
|
uri: "ui://widget/kanban-board.html",
|
|
mimeType: "text/html",
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(content)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
|
|
#expect(metaObject?["vendor.example/widgetPrefersBorder"] as? Bool == true)
|
|
|
|
let decoded = try JSONDecoder().decode(Resource.Content.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/widgetPrefersBorder"] == .bool(true))
|
|
}
|
|
|
|
@Test("Initialize.Result encoding with meta")
|
|
func testInitializeResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/build": .string("v1.0.0")])
|
|
|
|
let result = Initialize.Result(
|
|
protocolVersion: "2024-11-05",
|
|
capabilities: Server.Capabilities(),
|
|
serverInfo: Server.Info(name: "test", version: "1.0"),
|
|
instructions: "Test server",
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/build"] as? String == "v1.0.0")
|
|
|
|
let decoded = try JSONDecoder().decode(Initialize.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/build"] == .string("v1.0.0"))
|
|
}
|
|
|
|
@Test("ListTools.Result encoding with meta")
|
|
func testListToolsResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/page": .int(1)])
|
|
|
|
let tool = Tool(
|
|
name: "test",
|
|
description: "A test tool",
|
|
inputSchema: try Value(["type": "object"])
|
|
)
|
|
|
|
let result = ListTools.Result(
|
|
tools: [tool],
|
|
nextCursor: "page2",
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/page"] as? Int == 1)
|
|
|
|
let decoded = try JSONDecoder().decode(ListTools.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/page"] == .int(1))
|
|
}
|
|
|
|
@Test("CallTool.Result encoding with meta")
|
|
func testCallToolResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/executionTime": .int(150)])
|
|
|
|
let result = CallTool.Result(
|
|
content: [.text("Result data")],
|
|
isError: false,
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/executionTime"] as? Int == 150)
|
|
|
|
let decoded = try JSONDecoder().decode(CallTool.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/executionTime"] == .int(150))
|
|
}
|
|
|
|
@Test("ListResources.Result encoding with meta")
|
|
func testListResourcesResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/cacheControl": .string("max-age=3600")])
|
|
|
|
let resource = Resource(
|
|
name: "test.txt",
|
|
uri: "file://test.txt",
|
|
description: "Test resource",
|
|
mimeType: "text/plain"
|
|
)
|
|
|
|
let result = ListResources.Result(
|
|
resources: [resource],
|
|
nextCursor: nil,
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
|
|
|
|
let metaObject = json["_meta"] as! [String: Any]
|
|
#expect(metaObject["vendor.example/cacheControl"] as? String == "max-age=3600")
|
|
|
|
let decoded = try JSONDecoder().decode(ListResources.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/cacheControl"] == Value.string("max-age=3600"))
|
|
}
|
|
|
|
@Test("ReadResource.Result encoding with meta")
|
|
func testReadResourceResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/encoding": .string("utf-8")])
|
|
|
|
let result = ReadResource.Result(
|
|
contents: [.text("file contents", uri: "file://test.txt")],
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/encoding"] as? String == "utf-8")
|
|
|
|
let decoded = try JSONDecoder().decode(ReadResource.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/encoding"] == .string("utf-8"))
|
|
}
|
|
|
|
@Test("ListPrompts.Result encoding with meta")
|
|
func testListPromptsResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/category": .string("system")])
|
|
|
|
let prompt = Prompt(
|
|
name: "greeting",
|
|
description: "A greeting prompt"
|
|
)
|
|
|
|
let result = ListPrompts.Result(
|
|
prompts: [prompt],
|
|
nextCursor: nil,
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/category"] as? String == "system")
|
|
|
|
let decoded = try JSONDecoder().decode(ListPrompts.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/category"] == .string("system"))
|
|
}
|
|
|
|
@Test("GetPrompt.Result encoding with meta")
|
|
func testGetPromptResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/version": .int(2)])
|
|
|
|
let message = Prompt.Message.user("Hello")
|
|
|
|
let result = GetPrompt.Result(
|
|
description: "A test prompt",
|
|
messages: [message],
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as! [String: Any]
|
|
|
|
let metaObject = json["_meta"] as! [String: Any]
|
|
#expect(metaObject["vendor.example/version"] as? Int == 2)
|
|
|
|
let decoded = try JSONDecoder().decode(GetPrompt.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/version"] == Value.int(2))
|
|
}
|
|
|
|
@Test("CreateSamplingMessage.Result encoding with meta")
|
|
func testSamplingResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/model-version": .string("gpt-4-0613")])
|
|
|
|
let result = CreateSamplingMessage.Result(
|
|
model: "gpt-4",
|
|
stopReason: .endTurn,
|
|
role: .assistant,
|
|
content: .text("Hello!"),
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/model-version"] as? String == "gpt-4-0613")
|
|
|
|
let decoded = try JSONDecoder().decode(CreateSamplingMessage.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/model-version"] == .string("gpt-4-0613"))
|
|
}
|
|
|
|
@Test("CreateElicitation.Result encoding with meta")
|
|
func testElicitationResultGeneralFields() throws {
|
|
let meta = Metadata(additionalFields: ["vendor.example/timestamp": .int(1_640_000_000)])
|
|
|
|
let result = CreateElicitation.Result(
|
|
action: .accept,
|
|
content: ["response": .string("user input")],
|
|
_meta: meta
|
|
)
|
|
|
|
let data = try JSONEncoder().encode(result)
|
|
let json = try JSONSerialization.jsonObject(with: data) as? [String: Any]
|
|
|
|
let metaObject = json?["_meta"] as? [String: Any]
|
|
#expect(metaObject?["vendor.example/timestamp"] as? Int == 1_640_000_000)
|
|
|
|
let decoded = try JSONDecoder().decode(CreateElicitation.Result.self, from: data)
|
|
#expect(decoded._meta?["vendor.example/timestamp"] == .int(1_640_000_000))
|
|
}
|
|
}
|