From f4edb95d9f7757ea780ffcaf7fbfc005614953d2 Mon Sep 17 00:00:00 2001 From: EhsanAzish80 Date: Thu, 24 Sep 2026 18:41:24 +0300 Subject: [PATCH 1/4] Add standard MCP headers to HTTP client requests --- .../Base/Transports/HTTPClientTransport.swift | 41 +++++++++++++++++ .../HTTPServer/HTTPServerTypes.swift | 2 + Tests/MCPTests/HTTPClientTransportTests.swift | 46 +++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/Sources/MCP/Base/Transports/HTTPClientTransport.swift b/Sources/MCP/Base/Transports/HTTPClientTransport.swift index 060cd133..3768079b 100644 --- a/Sources/MCP/Base/Transports/HTTPClientTransport.swift +++ b/Sources/MCP/Base/Transports/HTTPClientTransport.swift @@ -250,6 +250,7 @@ public actor HTTPClientTransport: Transport { ) request.addValue(ContentType.json, forHTTPHeaderField: HTTPHeaderName.contentType) request.httpBody = data + addStandardRequestHeaders(to: &request, body: data) if let protocolVersion = protocolVersion { request.addValue(protocolVersion, forHTTPHeaderField: HTTPHeaderName.protocolVersion) @@ -304,6 +305,46 @@ public actor HTTPClientTransport: Transport { } } + /// Mirrors routing fields from a JSON-RPC request or notification into HTTP headers. + /// Responses and legacy batches have no single method to mirror. + private func addStandardRequestHeaders(to request: inout URLRequest, body: Data) { + guard let message = try? JSONSerialization.jsonObject(with: body) as? [String: Any], + let method = message["method"] as? String, + isSafeHTTPHeaderValue(method) + else { return } + + request.setValue(method, forHTTPHeaderField: HTTPHeaderName.method) + + let nameKey: String + switch method { + case "tools/call", "prompts/get": nameKey = "name" + case "resources/read": nameKey = "uri" + default: return + } + + guard let params = message["params"] as? [String: Any], + let name = params[nameKey] as? String + else { return } + + let headerValue: String + if isSafeHTTPHeaderValue(name), + !(name.hasPrefix("=?base64?") && name.hasSuffix("?=")) + { + headerValue = name + } else { + headerValue = "=?base64?\(Data(name.utf8).base64EncodedString())?=" + } + request.setValue(headerValue, forHTTPHeaderField: HTTPHeaderName.name) + } + + private func isSafeHTTPHeaderValue(_ value: String) -> Bool { + let bytes = value.utf8 + guard let first = bytes.first, let last = bytes.last, + first != 0x20, first != 0x09, last != 0x20, last != 0x09 + else { return false } + return bytes.allSatisfy { $0 == 0x09 || (0x20...0x7E).contains($0) } + } + #if os(Linux) private func processResponse(response: URLResponse, data: Data) async throws { guard let httpResponse = response as? HTTPURLResponse else { diff --git a/Sources/MCP/Base/Transports/HTTPServer/HTTPServerTypes.swift b/Sources/MCP/Base/Transports/HTTPServer/HTTPServerTypes.swift index bd56191c..0e5b6364 100644 --- a/Sources/MCP/Base/Transports/HTTPServer/HTTPServerTypes.swift +++ b/Sources/MCP/Base/Transports/HTTPServer/HTTPServerTypes.swift @@ -144,6 +144,8 @@ public enum HTTPResponse: Sendable { public enum HTTPHeaderName { public static let sessionID = "MCP-Session-Id" public static let protocolVersion = "MCP-Protocol-Version" + public static let method = "Mcp-Method" + public static let name = "Mcp-Name" public static let lastEventID = "Last-Event-ID" public static let accept = "Accept" public static let contentType = "Content-Type" diff --git a/Tests/MCPTests/HTTPClientTransportTests.swift b/Tests/MCPTests/HTTPClientTransportTests.swift index bd8c38f7..ebe30762 100644 --- a/Tests/MCPTests/HTTPClientTransportTests.swift +++ b/Tests/MCPTests/HTTPClientTransportTests.swift @@ -214,6 +214,52 @@ import Testing #expect(receivedData == responseData) } + @Test("POST request headers mirror the JSON-RPC method and name", .httpClientTransportSetup) + func testStandardRequestHeaders() async throws { + let configuration = URLSessionConfiguration.ephemeral + configuration.protocolClasses = [MockURLProtocol.self] + + let transport = HTTPClientTransport( + endpoint: testEndpoint, + configuration: configuration, + streaming: false + ) + try await transport.connect() + + let cases: [(body: String, method: String?, name: String?)] = [ + (#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#, "initialize", nil), + (#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#, + "notifications/initialized", nil), + (#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"weather"}}"#, + "tools/call", "weather"), + (#"{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"hello world"}}"#, + "prompts/get", "hello world"), + (#"{"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"file:///資料"}}"#, + "resources/read", "=?base64?ZmlsZTovLy/os4fmlpk=?="), + (#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"=?base64?abc?="}}"#, + "tools/call", "=?base64?PT9iYXNlNjQ/YWJjPz0=?="), + (#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":" weather "}}"#, + "tools/call", "=?base64?IHdlYXRoZXIg?="), + (#"{"jsonrpc":"2.0","id":7,"result":{}}"#, nil, nil), + ] + + for testCase in cases { + await MockURLProtocol.requestHandlerStorage.setHandler { + [testEndpoint] request in + #expect(request.url == testEndpoint) + #expect(request.value(forHTTPHeaderField: "Mcp-Method") == testCase.method) + #expect(request.value(forHTTPHeaderField: "Mcp-Name") == testCase.name) + + let response = HTTPURLResponse( + url: testEndpoint, statusCode: 202, httpVersion: "HTTP/1.1", + headerFields: [:])! + return (response, Data()) + } + try await transport.send(Data(testCase.body.utf8)) + } + await transport.disconnect() + } + @Test("Send and Receive Session ID", .httpClientTransportSetup) func testSendAndReceiveSessionID() async throws { let configuration = URLSessionConfiguration.ephemeral From 354b02d1282e616c0d981ebd4af6e8de70cf16b2 Mon Sep 17 00:00:00 2001 From: EhsanAzish80 Date: Sat, 3 Oct 2026 08:59:19 +0300 Subject: [PATCH 2/4] Encode tabs in MCP HTTP headers --- Sources/MCP/Base/Transports/HTTPClientTransport.swift | 2 +- Tests/MCPTests/HTTPClientTransportTests.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/MCP/Base/Transports/HTTPClientTransport.swift b/Sources/MCP/Base/Transports/HTTPClientTransport.swift index 3768079b..332bbba9 100644 --- a/Sources/MCP/Base/Transports/HTTPClientTransport.swift +++ b/Sources/MCP/Base/Transports/HTTPClientTransport.swift @@ -342,7 +342,7 @@ public actor HTTPClientTransport: Transport { guard let first = bytes.first, let last = bytes.last, first != 0x20, first != 0x09, last != 0x20, last != 0x09 else { return false } - return bytes.allSatisfy { $0 == 0x09 || (0x20...0x7E).contains($0) } + return bytes.allSatisfy { (0x20...0x7E).contains($0) } } #if os(Linux) diff --git a/Tests/MCPTests/HTTPClientTransportTests.swift b/Tests/MCPTests/HTTPClientTransportTests.swift index ebe30762..66799c97 100644 --- a/Tests/MCPTests/HTTPClientTransportTests.swift +++ b/Tests/MCPTests/HTTPClientTransportTests.swift @@ -240,6 +240,8 @@ import Testing "tools/call", "=?base64?PT9iYXNlNjQ/YWJjPz0=?="), (#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":" weather "}}"#, "tools/call", "=?base64?IHdlYXRoZXIg?="), + (#"{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"foo\tbar"}}"#, + "tools/call", "=?base64?Zm9vCWJhcg==?="), (#"{"jsonrpc":"2.0","id":7,"result":{}}"#, nil, nil), ] From fc0e7a15debedf01d743b71967ef6af4b5f3f726 Mon Sep 17 00:00:00 2001 From: EhsanAzish80 Date: Mon, 2 Nov 2026 09:26:29 +0300 Subject: [PATCH 3/4] Pin Swift 6.1 CI to Ubuntu 24.04 --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba30a965..2aa8e355 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: # Current leg: catches breakage from new Xcode and SDK releases. - os: macos-latest swift-version: Xcode default - - os: ubuntu-latest + - os: ubuntu-24.04 swift-version: 6.1.0 runs-on: ${{ matrix.os }} @@ -35,13 +35,13 @@ jobs: - uses: actions/checkout@v4 - name: Prepare Linux apt dependencies - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' run: | sudo -n apt-get update sudo -n apt-get install -y --no-install-recommends libcurl4-openssl-dev - name: Setup Swift on Linux - if: matrix.os == 'ubuntu-latest' + if: matrix.os == 'ubuntu-24.04' uses: vapor/swiftly-action@v0.2 with: toolchain: ${{ matrix.swift-version }} From 4a58c34e2722e40ce447140a85493dc626e7948f Mon Sep 17 00:00:00 2001 From: EhsanAzish80 Date: Mon, 2 Nov 2026 09:40:37 +0300 Subject: [PATCH 4/4] Wait for initialize request in client tests --- Tests/MCPTests/ClientTests.swift | 60 ++++++++-------------- Tests/MCPTests/Helpers/MockTransport.swift | 15 ++++++ 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/Tests/MCPTests/ClientTests.swift b/Tests/MCPTests/ClientTests.swift index 8a2882a4..4d2d8c71 100644 --- a/Tests/MCPTests/ClientTests.swift +++ b/Tests/MCPTests/ClientTests.swift @@ -14,10 +14,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -124,10 +122,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -174,10 +170,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -230,10 +224,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -321,10 +313,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -401,10 +391,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -481,10 +469,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -521,10 +507,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -656,10 +640,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, @@ -717,10 +699,8 @@ struct ClientTests { // Set up a task to handle the initialize response let initTask = Task { - try await Task.sleep(for: .milliseconds(10)) - if let lastMessage = await transport.sentMessages.last, - let data = lastMessage.data(using: .utf8), - let request = try? JSONDecoder().decode(Request.self, from: data) + let data = await transport.waitForFirstSentData() + if let request = try? JSONDecoder().decode(Request.self, from: data) { let response = Initialize.response( id: request.id, diff --git a/Tests/MCPTests/Helpers/MockTransport.swift b/Tests/MCPTests/Helpers/MockTransport.swift index 5979e9ad..2f45d8da 100644 --- a/Tests/MCPTests/Helpers/MockTransport.swift +++ b/Tests/MCPTests/Helpers/MockTransport.swift @@ -17,6 +17,7 @@ actor MockTransport: Transport { var isConnected = false private(set) var sentData: [Data] = [] + private var firstSendWaiters: [CheckedContinuation] = [] var sentMessages: [String] { return sentData.compactMap { data in guard let string = String(data: data, encoding: .utf8) else { @@ -57,6 +58,20 @@ actor MockTransport: Transport { throw MCPError.transportError(POSIXError(.EIO)) } sentData.append(message) + let waiters = firstSendWaiters + firstSendWaiters.removeAll() + for waiter in waiters { + waiter.resume(returning: message) + } + } + + func waitForFirstSentData() async -> Data { + if let data = sentData.first { + return data + } + return await withCheckedContinuation { continuation in + firstSendWaiters.append(continuation) + } } public func receive() -> AsyncThrowingStream {