Skip to content

Commit 58e4aa4

Browse files
dcherreraclaude
andcommitted
Fix bug modelcontextprotocol#14 & modelcontextprotocol#15: Remove blocking logger calls from ProcessTransport stderr + Add 30s timeout to Client.send()
Bug modelcontextprotocol#14: ProcessTransport.logStderr() had 2 blocking logger calls - logger.warning() and logger.error() cause actor isolation delays - Replaced with NSLog for synchronous stderr logging Bug modelcontextprotocol#15: Client.send() had no timeout mechanism - Requests would hang indefinitely if MCP server didn't respond - Added 30-second timeout using withThrowingTaskGroup - Properly cleans up pending requests on timeout - Logs timeout with clear error message These fixes allow us to: 1. See stderr output from MCP servers without delays 2. Detect and recover from hung MCP server processes 3. Provide better error messages to users when tools fail 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3ee8f1e commit 58e4aa4

File tree

1 file changed

+53
-30
lines changed

1 file changed

+53
-30
lines changed

Sources/MCP/Client/Client.swift

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -325,39 +325,62 @@ public actor Client {
325325
}
326326

327327
let requestData = try encoder.encode(request)
328-
NSLog("🔵 CLIENT.send() - Encoded request, entering continuation")
329-
330-
// Store the pending request first
331-
return try await withCheckedThrowingContinuation { continuation in
332-
NSLog("🔵 CLIENT.send() - Inside continuation, creating Task")
333-
Task {
334-
NSLog("🔵 CLIENT.send() - Task started, adding pending request")
335-
// Add the pending request before attempting to send
336-
self.addPendingRequest(
337-
id: request.id,
338-
continuation: continuation,
339-
type: M.Result.self
340-
)
341-
NSLog("🔵 CLIENT.send() - Pending request added, calling connection.send()")
342-
343-
// Send the request data
344-
do {
345-
// Use the existing connection send
346-
try await connection.send(requestData)
347-
NSLog("🔵 CLIENT.send() - connection.send() succeeded")
348-
} catch {
349-
NSLog("🔵 CLIENT.send() - connection.send() failed: \(error)")
350-
// If send fails, try to remove the pending request.
351-
// Resume with the send error only if we successfully removed the request,
352-
// indicating the response handler hasn't processed it yet.
353-
if self.removePendingRequest(id: request.id) != nil {
354-
continuation.resume(throwing: error)
328+
NSLog("🔵 CLIENT.send() - Encoded request, adding timeout wrapper")
329+
330+
// Add timeout wrapper (30 seconds) to prevent infinite hangs
331+
return try await withThrowingTaskGroup(of: M.Result.self) { group in
332+
// Task 1: Actual send operation
333+
group.addTask {
334+
return try await withCheckedThrowingContinuation { continuation in
335+
NSLog("🔵 CLIENT.send() - Inside continuation, creating Task")
336+
Task {
337+
NSLog("🔵 CLIENT.send() - Task started, adding pending request")
338+
// Add the pending request before attempting to send
339+
self.addPendingRequest(
340+
id: request.id,
341+
continuation: continuation,
342+
type: M.Result.self
343+
)
344+
NSLog("🔵 CLIENT.send() - Pending request added, calling connection.send()")
345+
346+
// Send the request data
347+
do {
348+
// Use the existing connection send
349+
try await connection.send(requestData)
350+
NSLog("🔵 CLIENT.send() - connection.send() succeeded, waiting for response...")
351+
} catch {
352+
NSLog("🔵 CLIENT.send() - connection.send() failed: \(error)")
353+
// If send fails, try to remove the pending request.
354+
// Resume with the send error only if we successfully removed the request,
355+
// indicating the response handler hasn't processed it yet.
356+
if self.removePendingRequest(id: request.id) != nil {
357+
continuation.resume(throwing: error)
358+
}
359+
// Otherwise, the request was already removed by the response handler
360+
// or by disconnect, so the continuation was already resumed.
361+
// Do nothing here.
362+
}
355363
}
356-
// Otherwise, the request was already removed by the response handler
357-
// or by disconnect, so the continuation was already resumed.
358-
// Do nothing here.
359364
}
360365
}
366+
367+
// Task 2: Timeout task (30 seconds)
368+
group.addTask {
369+
try await Task.sleep(nanoseconds: 30_000_000_000)
370+
NSLog("⏰ CLIENT.send() - TIMEOUT after 30 seconds for method: \(M.name)")
371+
throw MCPError.internalError("Request timed out after 30 seconds")
372+
}
373+
374+
// Wait for first task to complete (either success or timeout)
375+
do {
376+
let result = try await group.next()!
377+
group.cancelAll() // Cancel the other task
378+
return result
379+
} catch {
380+
// Clean up pending request on timeout
381+
_ = self.removePendingRequest(id: request.id)
382+
throw error
383+
}
361384
}
362385
}
363386

0 commit comments

Comments
 (0)