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
41 changes: 41 additions & 0 deletions Sources/MCP/Base/Transports/HTTPClientTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 { (0x20...0x7E).contains($0) }
}

#if os(Linux)
private func processResponse(response: URLResponse, data: Data) async throws {
guard let httpResponse = response as? HTTPURLResponse else {
Expand Down
2 changes: 2 additions & 0 deletions Sources/MCP/Base/Transports/HTTPServer/HTTPServerTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 48 additions & 0 deletions Tests/MCPTests/HTTPClientTransportTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,54 @@ 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":8,"method":"tools/call","params":{"name":"foo\tbar"}}"#,
"tools/call", "=?base64?Zm9vCWJhcg==?="),
(#"{"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
Expand Down