Skip to content

Commit c395237

Browse files
committed
Fixed issue where connection to /mcp got legacy endpoint downgraded
1 parent 409dd86 commit c395237

File tree

1 file changed

+84
-18
lines changed

1 file changed

+84
-18
lines changed

Sources/SwiftMCP/Client/MCPServerProxy.swift

Lines changed: 84 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public final actor MCPServerProxy: Sendable {
3434
private var isDisconnecting = false
3535

3636
public private(set) var endpointURL: URL?
37+
public private(set) var sessionID: String?
3738
private var streamTask: Task<Void, Error>?
3839

3940
public private(set) var serverName: String?
@@ -62,6 +63,8 @@ public final actor MCPServerProxy: Sendable {
6263
public func connect() async throws {
6364
isDisconnecting = false
6465
streamFailure = nil
66+
endpointURL = nil
67+
sessionID = nil
6568

6669
switch config {
6770
case .stdio(let stdioConfig):
@@ -96,17 +99,9 @@ public final actor MCPServerProxy: Sendable {
9699
let session = URLSession(configuration: sessionConfig)
97100
var request = URLRequest(url: sseConfig.url)
98101
request.httpMethod = "GET"
102+
applyConfiguredSSEHeaders(&request, sseConfig: sseConfig)
99103
request.setValue("text/event-stream", forHTTPHeaderField: "Accept")
100104

101-
for (key, value) in sseConfig.headers {
102-
request.setValue(value, forHTTPHeaderField: key)
103-
}
104-
105-
// Add Authorization header from meta if present
106-
if let accessToken = meta["accessToken"]?.value as? String {
107-
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
108-
}
109-
110105
let (asyncBytes, response) = try await session.bytes(for: request)
111106

112107
if let response = response as? HTTPURLResponse, response.statusCode != 200 {
@@ -116,10 +111,14 @@ public final actor MCPServerProxy: Sendable {
116111
throw MCPServerProxyError.communicationError("HTTP error \(response.statusCode): \(String(data: data, encoding: .utf8) ?? "Unknown error")")
117112
}
118113

119-
if let response = response as? HTTPURLResponse,
120-
let sessionId = response.value(forHTTPHeaderField: "Mcp-Session-Id"),
121-
let endpoint = messageEndpointURL(baseURL: sseConfig.url, sessionId: sessionId) {
122-
endpointURL = endpoint
114+
if let response = response as? HTTPURLResponse {
115+
sessionID = response.value(forHTTPHeaderField: "Mcp-Session-Id")
116+
if isStreamableMCPURL(sseConfig.url) {
117+
endpointURL = sseConfig.url
118+
} else if let sessionID,
119+
let endpoint = messageEndpointURL(baseURL: sseConfig.url, sessionId: sessionID) {
120+
endpointURL = endpoint
121+
}
123122
}
124123

125124
streamTask = Task {
@@ -175,6 +174,8 @@ public final actor MCPServerProxy: Sendable {
175174

176175
streamTask?.cancel()
177176
streamTask = nil
177+
endpointURL = nil
178+
sessionID = nil
178179

179180
switch config {
180181
case .stdio, .stdioHandles, .tcp:
@@ -342,7 +343,7 @@ public final actor MCPServerProxy: Sendable {
342343
}
343344
}
344345

345-
case .sse:
346+
case .sse(let sseConfig):
346347
guard let endpointURL = endpointURL else {
347348
throw MCPServerProxyError.communicationError("Not connected to server")
348349
}
@@ -356,6 +357,7 @@ public final actor MCPServerProxy: Sendable {
356357
var urlRequest = URLRequest(url: endpointURL)
357358
urlRequest.httpMethod = "POST"
358359
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
360+
configureSSEPOSTRequest(&urlRequest, sseConfig: sseConfig)
359361
let encoder = JSONEncoder()
360362
encoder.outputFormatting = [.sortedKeys]
361363
let data = try encoder.encode(message)
@@ -371,13 +373,29 @@ public final actor MCPServerProxy: Sendable {
371373

372374
Task {
373375
do {
374-
let (_, response) = try await session.data(for: urlRequest)
375-
if let httpResponse = response as? HTTPURLResponse,
376-
![200, 202].contains(httpResponse.statusCode) {
376+
let (responseData, response) = try await session.data(for: urlRequest)
377+
guard let httpResponse = response as? HTTPURLResponse else {
378+
throw MCPServerProxyError.communicationError("Invalid HTTP response")
379+
}
380+
381+
switch httpResponse.statusCode {
382+
case 200:
383+
guard let responseMessage = try responseMessage(for: messageId, from: responseData) else {
384+
let responseBody = String(data: responseData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
385+
let details = responseBody.isEmpty ? "" : ": \(responseBody)"
386+
throw MCPServerProxyError.communicationError("HTTP 200 did not include JSON-RPC response for request \(messageId.stringValue)\(details)")
387+
}
388+
377389
if responseTasks[messageId] != nil {
378390
responseTasks.removeValue(forKey: messageId)
379-
continuation.resume(throwing: MCPServerProxyError.communicationError("HTTP error \(httpResponse.statusCode)"))
391+
continuation.resume(returning: responseMessage)
380392
}
393+
case 202:
394+
break // response will arrive over SSE
395+
default:
396+
let responseBody = String(data: responseData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
397+
let details = responseBody.isEmpty ? "" : ": \(responseBody)"
398+
throw MCPServerProxyError.communicationError("HTTP error \(httpResponse.statusCode)\(details)")
381399
}
382400
} catch {
383401
if responseTasks[messageId] != nil {
@@ -560,12 +578,14 @@ public final actor MCPServerProxy: Sendable {
560578

561579
private func handlePingRequest(_ request: JSONRPCMessage.JSONRPCRequestData) async {
562580
guard let endpointURL = endpointURL else { return }
581+
guard case .sse(let sseConfig) = config else { return }
563582
let response = JSONRPCMessage.response(id: request.id, result: [:])
564583
let sessionConfig = URLSessionConfiguration.default
565584
let session = URLSession(configuration: sessionConfig)
566585
var urlRequest = URLRequest(url: endpointURL)
567586
urlRequest.httpMethod = "POST"
568587
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
588+
configureSSEPOSTRequest(&urlRequest, sseConfig: sseConfig)
569589
let encoder = JSONEncoder()
570590
let data = try? encoder.encode(response)
571591
urlRequest.httpBody = data
@@ -767,6 +787,52 @@ public final actor MCPServerProxy: Sendable {
767787
return "\(rounded)%"
768788
}
769789

790+
private func isStreamableMCPURL(_ url: URL) -> Bool {
791+
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
792+
return false
793+
}
794+
return components.path.hasPrefix("/mcp")
795+
}
796+
797+
private func applyConfiguredSSEHeaders(_ request: inout URLRequest, sseConfig: MCPServerSseConfig) {
798+
for (key, value) in sseConfig.headers {
799+
request.setValue(value, forHTTPHeaderField: key)
800+
}
801+
802+
// Allow token in metadata to override auth header from config.
803+
if let accessToken = meta["accessToken"]?.value as? String {
804+
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
805+
}
806+
}
807+
808+
private func configureSSEPOSTRequest(_ request: inout URLRequest, sseConfig: MCPServerSseConfig) {
809+
applyConfiguredSSEHeaders(&request, sseConfig: sseConfig)
810+
request.setValue("application/json, text/event-stream", forHTTPHeaderField: "Accept")
811+
812+
if let sessionID {
813+
request.setValue(sessionID, forHTTPHeaderField: "Mcp-Session-Id")
814+
}
815+
}
816+
817+
private func responseMessage(for requestID: JSONRPCID, from data: Data) throws -> JSONRPCMessage? {
818+
guard !data.isEmpty else {
819+
return nil
820+
}
821+
822+
let messages = try JSONRPCMessage.decodeMessages(from: data)
823+
return messages.first { message in
824+
guard message.id == requestID else {
825+
return false
826+
}
827+
switch message {
828+
case .response, .errorResponse:
829+
return true
830+
case .request, .notification:
831+
return false
832+
}
833+
}
834+
}
835+
770836
private func messageEndpointURL(baseURL: URL, sessionId: String) -> URL? {
771837
guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else {
772838
return nil

0 commit comments

Comments
 (0)