@@ -2,6 +2,9 @@ import Foundation
22import SwiftUI
33import MCP
44import 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 = [
@@ -134,7 +146,12 @@ class McpHostViewModel: ObservableObject {
134146 throw ToolCallError . invalidJson
135147 }
136148
149+ let serverName = selectedServerIndex >= 0 && selectedServerIndex < Self . knownServers. count
150+ ? Self . knownServers [ selectedServerIndex] . 0
151+ : " Custom Server "
152+
137153 let toolCallInfo = ToolCallInfo (
154+ serverName: serverName,
138155 tool: tool,
139156 input: inputDict,
140157 client: client,
@@ -234,7 +251,10 @@ class McpHostViewModel: ObservableObject {
234251 }
235252 }
236253
237- func removeToolCall( _ toolCall: ToolCallInfo ) {
254+ func removeToolCall( _ toolCall: ToolCallInfo ) async {
255+ if let error = await toolCall. teardown ( ) {
256+ showToast ( error)
257+ }
238258 activeToolCalls. removeAll { $0. id == toolCall. id }
239259 }
240260}
@@ -268,6 +288,7 @@ struct ToolResult {
268288@MainActor
269289class ToolCallInfo : ObservableObject , Identifiable {
270290 let id = UUID ( )
291+ let serverName : String
271292 let tool : Tool
272293 let input : [ String : Any ]
273294 let client : Client
@@ -281,14 +302,17 @@ class ToolCallInfo: ObservableObject, Identifiable {
281302 @Published var result : ToolResult ?
282303 @Published var state : ExecutionState = . calling
283304 @Published var error : String ?
305+ @Published var isTearingDown = false
284306
285307 init (
308+ serverName: String ,
286309 tool: Tool ,
287310 input: [ String : Any ] ,
288311 client: Client ,
289312 hostInfo: Implementation ,
290313 hostCapabilities: McpUiHostCapabilities
291314 ) {
315+ self . serverName = serverName
292316 self . tool = tool
293317 self . input = input
294318 self . client = client
@@ -374,37 +398,38 @@ class ToolCallInfo: ObservableObject, Identifiable {
374398 bridge. onInitialized = { [ weak self] in
375399 Task { @MainActor in
376400 guard let self = self else { return }
377- try ? await bridge . sendToolInput (
401+ let params = McpUiToolInputParams (
378402 arguments: self . input. mapValues { AnyCodable ( $0) }
379403 )
404+ try ? await bridge. sendToolInput ( params)
380405 if let result = self . result {
381406 try ? await self . sendToolResult ( result, to: bridge)
382407 }
383408 }
384409 }
385410
386- bridge. onMessage = { role , content in
387- print ( " [Host] Message from Guest UI: \( role) " )
411+ bridge. onMessage = { params in
412+ print ( " [Host] Message from Guest UI: \( params . role) " )
388413 return McpUiMessageResult ( isError: false )
389414 }
390415
391- bridge. onOpenLink = { url in
392- print ( " [Host] Open link request: \( url) " )
393- if let urlObj = URL ( string: url) {
416+ bridge. onOpenLink = { params in
417+ print ( " [Host] Open link request: \( params . url) " )
418+ if let urlObj = URL ( string: params . url) {
394419 await MainActor . run {
395420 UIApplication . shared. open ( urlObj)
396421 }
397422 }
398423 return McpUiOpenLinkResult ( isError: false )
399424 }
400425
401- bridge. onLoggingMessage = { level , data , logger in
402- print ( " [Host] Guest UI log [ \( level) ]: \( data. value) " )
426+ bridge. onLoggingMessage = { params in
427+ print ( " [Host] Guest UI log [ \( params . level) ]: \( params . data. value) " )
403428 }
404429
405- bridge. onSizeChange = { [ weak self] width , height in
406- print ( " [Host] Size change: \( width ?? 0 ) x \( height ?? 0 ) " )
407- if let height = height {
430+ bridge. onSizeChange = { [ weak self] params in
431+ print ( " [Host] Size change: \( params . width ?? 0 ) x \( params . height ?? 0 ) " )
432+ if let height = params . height {
408433 Task { @MainActor in
409434 self ? . preferredHeight = CGFloat ( height)
410435 }
@@ -455,24 +480,66 @@ class ToolCallInfo: ObservableObject, Identifiable {
455480 ]
456481 }
457482
483+ logger. info ( " Connecting bridge to transport... " )
458484 try await bridge. connect ( transport)
459485 self . appBridge = bridge
486+ logger. info ( " AppBridge is now set and ready " )
460487 }
461488
462489 private func sendToolResult( _ result: ToolResult , to bridge: AppBridge ) async throws {
463- try await bridge. sendToolResult ( [
464- " content " : AnyCodable ( result. content. map { c -> [ String : Any ] in
465- switch c {
466- case . text( let text) :
467- return [ " type " : " text " , " text " : text]
468- case . image( let data, let mimeType, _) :
469- return [ " type " : " image " , " data " : data, " mimeType " : mimeType]
470- default :
471- return [ " type " : " text " , " text " : " " ]
490+ let contentItems : [ McpUiToolResultNotificationParamsContentItem ] = result. content. compactMap { c in
491+ switch c {
492+ case . text( let text) :
493+ return . text( McpUiToolResultNotificationParamsContentItemText ( type: " text " , text: text) )
494+ case . image( let data, let mimeType, _) :
495+ return . image( McpUiToolResultNotificationParamsContentItemImage ( type: " image " , data: data, mimeType: mimeType) )
496+ default :
497+ return nil
498+ }
499+ }
500+ let params = McpUiToolResultParams ( content: contentItems, isError: result. isError)
501+ try await bridge. sendToolResult ( params)
502+ }
503+
504+ /// Teardown the app bridge before removing the tool call
505+ /// Returns an error message if teardown failed, nil on success
506+ func teardown( ) async -> String ? {
507+ // Prevent double-tap
508+ guard !isTearingDown else {
509+ logger. info ( " Teardown already in progress, skipping " )
510+ return nil
511+ }
512+ isTearingDown = true
513+ logger. info ( " Starting teardown, appBridge exists: \( self . appBridge != nil ) " )
514+
515+ var errorMessage : String ?
516+
517+ if let bridge = appBridge {
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 "
472529 }
473- } ) ,
474- " isError " : AnyCodable ( result. isError ?? false )
475- ] )
530+ } else {
531+ logger. info ( " Skipping teardown - bridge not yet initialized " )
532+ }
533+
534+ logger. info ( " Closing bridge... " )
535+ await bridge. close ( )
536+ logger. info ( " Bridge closed " )
537+ } else {
538+ logger. warning ( " No bridge to teardown (appBridge is nil) " )
539+ }
540+ appBridge = nil
541+ logger. info ( " Teardown complete, will remove card from list " )
542+ return errorMessage
476543 }
477544}
478545
0 commit comments