Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Sources/MCP/Client/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,24 @@ public actor Client {
/// Whether the client supports elicitation
public var elicitation: Elicitation?
/// Experimental features supported by the client
public var experimental: [String: String]?
public var experimental: [String: Value]?
/// Capability extensions declared by the client,
/// such as MCP Apps UI extensions
public var extensions: [String: Value]?
/// Whether the client supports roots
public var roots: Capabilities.Roots?

public init(
sampling: Sampling? = nil,
elicitation: Elicitation? = nil,
experimental: [String: String]? = nil,
experimental: [String: Value]? = nil,
extensions: [String: Value]? = nil,
roots: Capabilities.Roots? = nil
) {
self.sampling = sampling
self.elicitation = elicitation
self.experimental = experimental
self.extensions = extensions
self.roots = roots
}
}
Expand Down
55 changes: 55 additions & 0 deletions Tests/MCPTests/RequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,61 @@ struct RequestTests {
#expect(decoded.params.clientInfo.name == "unknown")
}

@Test("Initialize parameters decoding - nested experimental and extensions")
func testInitializeParametersDecodingNestedCapabilities() throws {
let jsonString = """
{
"protocolVersion": "2025-11-25",
"capabilities": {
"experimental": {
"openai/visibility": { "enabled": true }
},
"extensions": {
"io.modelcontextprotocol/ui": {
"mimeTypes": ["text/html;profile=mcp-app"]
}
}
},
"clientInfo": { "name": "openai-mcp", "version": "1.0.0" }
}
"""
let data = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()
let decoded = try decoder.decode(Initialize.Parameters.self, from: data)

#expect(decoded.protocolVersion == "2025-11-25")
#expect(decoded.clientInfo.name == "openai-mcp")
#expect(decoded.clientInfo.version == "1.0.0")

#expect(
decoded.capabilities.experimental?["openai/visibility"]
== .object(["enabled": .bool(true)]))
#expect(
decoded.capabilities.extensions?["io.modelcontextprotocol/ui"]
== .object(["mimeTypes": .array([.string("text/html;profile=mcp-app")])]))
}

@Test("Client capabilities round-trip with experimental and extensions")
func testClientCapabilitiesExperimentalExtensionsRoundTrip() throws {
let capabilities = Client.Capabilities(
experimental: ["openai/visibility": .object(["enabled": .bool(true)])],
extensions: [
"io.modelcontextprotocol/ui": .object([
"mimeTypes": .array([.string("text/html;profile=mcp-app")])
])
]
)

let encoder = JSONEncoder()
let decoder = JSONDecoder()

let data = try encoder.encode(capabilities)
let decoded = try decoder.decode(Client.Capabilities.self, from: data)

#expect(decoded == capabilities)
}

@Test("Invalid parameters request decoding")
func testInvalidParametersRequestDecoding() throws {
let jsonString = """
Expand Down