Skip to content

Commit 2268d5e

Browse files
ochafikclaude
andcommitted
feat(swift-host): Add toast notifications for teardown errors
- Add toastMessage property and showToast() method to ViewModel - Add toast overlay at bottom of screen (auto-dismiss after 3s) - Change teardown() to return optional error message - Show toast when teardown fails or times out - Add OSLog-based logging for debugging - Improve logs.sh script for better log filtering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b2ff5db commit 2268d5e

File tree

3 files changed

+72
-21
lines changed

3 files changed

+72
-21
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,20 @@ struct ContentView: View {
4747
}
4848
.navigationTitle("MCP Host")
4949
.navigationBarTitleDisplayMode(.inline)
50+
.overlay(alignment: .bottom) {
51+
if let toast = viewModel.toastMessage {
52+
Text(toast)
53+
.font(.footnote)
54+
.foregroundColor(.white)
55+
.padding(.horizontal, 16)
56+
.padding(.vertical, 10)
57+
.background(Color.red.opacity(0.9))
58+
.cornerRadius(8)
59+
.padding(.bottom, 80) // Above toolbar
60+
.transition(.move(edge: .bottom).combined(with: .opacity))
61+
.animation(.easeInOut, value: viewModel.toastMessage)
62+
}
63+
}
5064
}
5165
}
5266

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

Lines changed: 56 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Foundation
22
import SwiftUI
33
import MCP
44
import McpApps
5+
import os.log
6+
7+
private let logger = Logger(subsystem: "com.example.BasicHostSwift", category: "ToolCall")
58

69
/// View model managing MCP server connection and tool execution.
710
@MainActor
@@ -22,6 +25,15 @@ class McpHostViewModel: ObservableObject {
2225
}
2326
@Published var activeToolCalls: [ToolCallInfo] = []
2427
@Published var errorMessage: String?
28+
@Published var toastMessage: String?
29+
30+
func showToast(_ message: String, duration: TimeInterval = 3.0) {
31+
toastMessage = message
32+
Task {
33+
try? await Task.sleep(nanoseconds: UInt64(duration * 1_000_000_000))
34+
await MainActor.run { self.toastMessage = nil }
35+
}
36+
}
2537

2638
/// Known MCP servers (matches examples/servers.json)
2739
static let knownServers = [
@@ -240,7 +252,9 @@ class McpHostViewModel: ObservableObject {
240252
}
241253

242254
func removeToolCall(_ toolCall: ToolCallInfo) async {
243-
await toolCall.teardown()
255+
if let error = await toolCall.teardown() {
256+
showToast(error)
257+
}
244258
activeToolCalls.removeAll { $0.id == toolCall.id }
245259
}
246260
}
@@ -465,8 +479,10 @@ class ToolCallInfo: ObservableObject, Identifiable {
465479
]
466480
}
467481

482+
logger.info("Connecting bridge to transport...")
468483
try await bridge.connect(transport)
469484
self.appBridge = bridge
485+
logger.info("AppBridge is now set and ready")
470486
}
471487

472488
private func sendToolResult(_ result: ToolResult, to bridge: AppBridge) async throws {
@@ -486,33 +502,56 @@ class ToolCallInfo: ObservableObject, Identifiable {
486502
}
487503

488504
/// Teardown the app bridge before removing the tool call
489-
func teardown() async {
505+
/// Returns an error message if teardown failed, nil on success
506+
func teardown() async -> String? {
490507
// Prevent double-tap
491-
guard !isTearingDown else { return }
508+
guard !isTearingDown else {
509+
logger.info("Teardown already in progress, skipping")
510+
return nil
511+
}
492512
isTearingDown = true
513+
logger.info("Starting teardown, appBridge exists: \(self.appBridge != nil)")
514+
515+
var errorMessage: String?
493516

494517
if let bridge = appBridge {
495-
do {
496-
// Use a timeout so we don't wait forever if app is unresponsive
497-
try await withThrowingTaskGroup(of: Void.self) { group in
498-
group.addTask {
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()...")
499525
_ = 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"
500531
}
501-
group.addTask {
502-
try await Task.sleep(nanoseconds: 5_000_000_000) // 5 second timeout
503-
throw CancellationError()
504-
}
505-
// Wait for first to complete (either teardown or timeout)
506-
try await group.next()
507-
group.cancelAll()
508532
}
509-
} catch {
510-
print("[Host] Teardown failed or timed out: \(error)")
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"
537+
}
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()
511544
}
545+
546+
logger.info("Closing bridge...")
512547
await bridge.close()
548+
logger.info("Bridge closed")
549+
} else {
550+
logger.warning("No bridge to teardown (appBridge is nil)")
513551
}
514552
appBridge = nil
515-
// Note: isTearingDown stays true - the card will be removed from the list
553+
logger.info("Teardown complete, will remove card from list")
554+
return errorMessage
516555
}
517556
}
518557

examples/basic-host-swift/scripts/logs.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ echo "📋 Streaming logs from $BUNDLE_ID on '$SIMULATOR'..."
99
echo " Press Ctrl+C to stop"
1010
echo ""
1111

12-
# Stream logs, filtering for our app
12+
# Stream logs, filtering for our app (both OSLog and print statements)
1313
xcrun simctl spawn "$SIMULATOR" log stream \
1414
--predicate "subsystem == '$BUNDLE_ID' OR process == 'BasicHostSwift'" \
15-
--style compact 2>/dev/null || \
16-
xcrun simctl spawn "$SIMULATOR" log stream \
17-
--predicate "process == 'BasicHostSwift'" \
15+
--level debug \
1816
--style compact

0 commit comments

Comments
 (0)