From 00f1ffa819463773de303b1ba4d30d8d2a930050 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 7 May 2025 05:45:07 -0700 Subject: [PATCH 1/3] Implement client version negotiation in server initialization --- Sources/MCP/Base/Versioning.swift | 21 +++++++++++++++++++-- Sources/MCP/Server/Server.swift | 11 ++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Sources/MCP/Base/Versioning.swift b/Sources/MCP/Base/Versioning.swift index 44142fe3..bbf1a4a5 100644 --- a/Sources/MCP/Base/Versioning.swift +++ b/Sources/MCP/Base/Versioning.swift @@ -6,6 +6,23 @@ import Foundation /// /// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-03-26/ public enum Version { - /// The current protocol version. - public static let latest = "2025-03-26" + /// All protocol versions supported by this implementation, ordered from newest to oldest. + public static let supported: Set = [ + "2025-03-26", + "2024-11-05", + ] + + /// The latest protocol version supported by this implementation. + public static let latest = supported.max()! + + /// Negotiates the protocol version based on the client's request and server's capabilities. + /// - Parameter clientRequestedVersion: The protocol version requested by the client. + /// - Returns: The negotiated protocol version. If the client's requested version is supported, + /// that version is returned. Otherwise, the server's latest supported version is returned. + public static func negotiate(clientRequestedVersion: String) -> String { + if supported.contains(clientRequestedVersion) { + return clientRequestedVersion + } + return latest + } } diff --git a/Sources/MCP/Server/Server.swift b/Sources/MCP/Server/Server.swift index 945698d9..15ad3db6 100644 --- a/Sources/MCP/Server/Server.swift +++ b/Sources/MCP/Server/Server.swift @@ -486,15 +486,20 @@ public actor Server { try await hook(params.clientInfo, params.capabilities) } - // Set initial state + // Perform version negotiation + let clientRequestedVersion = params.protocolVersion + let negotiatedProtocolVersion = Version.negotiate( + clientRequestedVersion: clientRequestedVersion) + + // Set initial state with the negotiated protocol version await self.setInitialState( clientInfo: params.clientInfo, clientCapabilities: params.capabilities, - protocolVersion: params.protocolVersion + protocolVersion: negotiatedProtocolVersion ) return Initialize.Result( - protocolVersion: Version.latest, + protocolVersion: negotiatedProtocolVersion, capabilities: await self.capabilities, serverInfo: self.serverInfo, instructions: nil From cdf7edd4e71b9d1f61a4ace81a4e3b080056be25 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 7 May 2025 05:50:14 -0700 Subject: [PATCH 2/3] Add test coverage for version negotiation --- Tests/MCPTests/VersioningTests.swift | 53 ++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 Tests/MCPTests/VersioningTests.swift diff --git a/Tests/MCPTests/VersioningTests.swift b/Tests/MCPTests/VersioningTests.swift new file mode 100644 index 00000000..d1896b53 --- /dev/null +++ b/Tests/MCPTests/VersioningTests.swift @@ -0,0 +1,53 @@ +import Testing + +@testable import MCP + +@Suite("Version Negotiation Tests") +struct VersioningTests { + @Test("Client requests latest supported version") + func testClientRequestsLatestSupportedVersion() { + let clientVersion = Version.latest + let negotiatedVersion = Version.negotiate(clientRequestedVersion: clientVersion) + #expect(negotiatedVersion == Version.latest) + } + + @Test("Client requests older supported version") + func testClientRequestsOlderSupportedVersion() { + let clientVersion = "2024-11-05" + let negotiatedVersion = Version.negotiate(clientRequestedVersion: clientVersion) + #expect(negotiatedVersion == "2024-11-05") + } + + @Test("Client requests unsupported version") + func testClientRequestsUnsupportedVersion() { + let clientVersion = "2023-01-01" // An unsupported version + let negotiatedVersion = Version.negotiate(clientRequestedVersion: clientVersion) + #expect(negotiatedVersion == Version.latest) + } + + @Test("Client requests empty version string") + func testClientRequestsEmptyVersionString() { + let clientVersion = "" + let negotiatedVersion = Version.negotiate(clientRequestedVersion: clientVersion) + #expect(negotiatedVersion == Version.latest) + } + + @Test("Client requests garbage version string") + func testClientRequestsGarbageVersionString() { + let clientVersion = "not-a-version" + let negotiatedVersion = Version.negotiate(clientRequestedVersion: clientVersion) + #expect(negotiatedVersion == Version.latest) + } + + @Test("Server's supported versions correctly defined") + func testServerSupportedVersions() { + #expect(Version.supported.contains("2025-03-26")) + #expect(Version.supported.contains("2024-11-05")) + #expect(Version.supported.count == 2) + } + + @Test("Server's latest version is correct") + func testServerLatestVersion() { + #expect(Version.latest == "2025-03-26") + } +} From eb683955c5a479e8b7d0d88ca8d0aaf4b7c73763 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Wed, 7 May 2025 05:56:31 -0700 Subject: [PATCH 3/3] Make Version.supported and Version.negotiate() APIs internal to not create new surface area --- Sources/MCP/Base/Versioning.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/MCP/Base/Versioning.swift b/Sources/MCP/Base/Versioning.swift index bbf1a4a5..05c77a00 100644 --- a/Sources/MCP/Base/Versioning.swift +++ b/Sources/MCP/Base/Versioning.swift @@ -7,7 +7,7 @@ import Foundation /// - SeeAlso: https://spec.modelcontextprotocol.io/specification/2025-03-26/ public enum Version { /// All protocol versions supported by this implementation, ordered from newest to oldest. - public static let supported: Set = [ + static let supported: Set = [ "2025-03-26", "2024-11-05", ] @@ -19,7 +19,7 @@ public enum Version { /// - Parameter clientRequestedVersion: The protocol version requested by the client. /// - Returns: The negotiated protocol version. If the client's requested version is supported, /// that version is returned. Otherwise, the server's latest supported version is returned. - public static func negotiate(clientRequestedVersion: String) -> String { + static func negotiate(clientRequestedVersion: String) -> String { if supported.contains(clientRequestedVersion) { return clientRequestedVersion }