Skip to content

Commit 8295077

Browse files
ochafikclaude
andcommitted
fix(swift): Add request timeout and initialization check for teardown
SDK changes: - Add timeout parameter to sendRequest (default 30s) - sendResourceTeardown uses 5s timeout - Add failPendingRequest helper for clean timeout handling Host changes: - Check isReady() before sending teardown - Skip teardown if bridge not yet initialized (no data to save) - Simplified logic since SDK handles timeout internally This prevents cards from getting stuck when closed before initialization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 2268d5e commit 8295077

File tree

2 files changed

+35
-36
lines changed

2 files changed

+35
-36
lines changed

examples/basic-host-swift/Sources/BasicHostApp/McpHostViewModel.swift

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -515,32 +515,20 @@ class ToolCallInfo: ObservableObject, Identifiable {
515515
var errorMessage: String?
516516

517517
if let bridge = appBridge {
518-
logger.info("Sending teardown request...")
519-
520-
// Race between teardown and timeout
521-
await withTaskGroup(of: String?.self) { group in
522-
group.addTask {
523-
do {
524-
logger.info("Calling bridge.sendResourceTeardown()...")
525-
_ = try await bridge.sendResourceTeardown()
526-
logger.info("Teardown request completed successfully")
527-
return nil
528-
} catch {
529-
logger.error("Teardown request failed: \(String(describing: error)), type: \(type(of: error))")
530-
return "Teardown failed: app may not have saved data"
531-
}
532-
}
533-
group.addTask {
534-
try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 second timeout
535-
logger.warning("Teardown timeout reached after 5 seconds")
536-
return "Teardown timed out: app may not have saved data"
518+
// Only send teardown if the bridge is initialized
519+
// If not initialized, the app hasn't done anything worth saving
520+
let isReady = await bridge.isReady()
521+
if isReady {
522+
logger.info("Sending teardown request...")
523+
do {
524+
_ = try await bridge.sendResourceTeardown()
525+
logger.info("Teardown request completed successfully")
526+
} catch {
527+
logger.error("Teardown request failed: \(String(describing: error))")
528+
errorMessage = "Teardown failed: app may not have saved data"
537529
}
538-
// Wait for first to complete
539-
if let result = await group.next() {
540-
errorMessage = result
541-
}
542-
logger.info("Task group completed")
543-
group.cancelAll()
530+
} else {
531+
logger.info("Skipping teardown - bridge not yet initialized")
544532
}
545533

546534
logger.info("Closing bridge...")

swift/Sources/McpApps/AppBridge.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,8 @@ public actor AppBridge {
231231
params: dict.mapValues { AnyCodable($0) })
232232
}
233233

234-
public func sendResourceTeardown() async throws -> McpUiResourceTeardownResult {
235-
_ = try await sendRequest(method: "ui/resource-teardown", params: [:])
234+
public func sendResourceTeardown(timeout: TimeInterval = 5) async throws -> McpUiResourceTeardownResult {
235+
_ = try await sendRequest(method: "ui/resource-teardown", params: [:], timeout: timeout)
236236
return McpUiResourceTeardownResult()
237237
}
238238

@@ -242,29 +242,40 @@ public actor AppBridge {
242242
try await transport?.send(.notification(JSONRPCNotification(method: method, params: params)))
243243
}
244244

245-
private func sendRequest(method: String, params: [String: AnyCodable]?) async throws -> AnyCodable {
245+
private func sendRequest(method: String, params: [String: AnyCodable]?, timeout: TimeInterval = 30) async throws -> AnyCodable {
246246
let id = JSONRPCId.number(nextRequestId)
247247
nextRequestId += 1
248248
let request = JSONRPCRequest(id: id, method: method, params: params)
249249

250-
guard transport != nil else {
251-
print("[AppBridge] sendRequest failed: transport is nil")
250+
guard let transport = transport else {
252251
throw BridgeError.disconnected
253252
}
254253

255-
return try await withCheckedThrowingContinuation { continuation in
254+
// Create the continuation and store it
255+
let result: AnyCodable = try await withCheckedThrowingContinuation { continuation in
256256
pendingRequests[id] = continuation
257+
258+
// Start timeout task
259+
Task {
260+
try? await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
261+
// If still pending after timeout, fail it
262+
await self.failPendingRequest(id: id, error: BridgeError.timeout)
263+
}
264+
265+
// Send the request
257266
Task {
258267
do {
259-
print("[AppBridge] Sending request: \(method)")
260-
try await transport?.send(.request(request))
261-
print("[AppBridge] Request sent successfully, waiting for response...")
268+
try await transport.send(.request(request))
262269
} catch {
263-
print("[AppBridge] Transport send failed: \(error)")
264-
pendingRequests.removeValue(forKey: id)?.resume(throwing: error)
270+
await self.failPendingRequest(id: id, error: error)
265271
}
266272
}
267273
}
274+
return result
275+
}
276+
277+
private func failPendingRequest(id: JSONRPCId, error: Error) {
278+
pendingRequests.removeValue(forKey: id)?.resume(throwing: error)
268279
}
269280
}
270281

0 commit comments

Comments
 (0)