Skip to content

Commit 5f0a3b0

Browse files
ochafikclaude
andcommitted
feat(swift-host): add structuredContent support + JS console logging
- Use client.send() directly to get full CallTool.Result including structuredContent (the SDK's callTool() helper drops structuredContent) - Add Value.toAny() extension to convert MCP Value to Any for AnyCodable - Update ToolResult to include structuredContent field - Pass structuredContent to AppBridge.sendToolResult() when present - Add WKScriptMessageHandler to capture JavaScript console.log/error/warn from WebView and output as [JS LOG/ERROR/WARN] messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent ac927cd commit 5f0a3b0

File tree

2 files changed

+70
-9
lines changed

2 files changed

+70
-9
lines changed

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

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ import os.log
66

77
private let logger = Logger(subsystem: "com.example.BasicHostSwift", category: "ToolCall")
88

9+
// Helper extension to convert MCP Value to Any for use with AnyCodable
10+
extension Value {
11+
func toAny() -> Any {
12+
switch self {
13+
case .null: return NSNull()
14+
case .bool(let b): return b
15+
case .int(let i): return i
16+
case .double(let d): return d
17+
case .string(let s): return s
18+
case .data(_, let d): return d.base64EncodedString()
19+
case .array(let arr): return arr.map { $0.toAny() }
20+
case .object(let dict): return dict.mapValues { $0.toAny() }
21+
}
22+
}
23+
}
24+
925
/// View model managing MCP server connection and tool execution.
1026
@MainActor
1127
class McpHostViewModel: ObservableObject {
@@ -282,6 +298,7 @@ enum ConnectionState: Equatable {
282298
/// Tool result type matching MCP SDK's callTool return
283299
struct ToolResult {
284300
let content: [Tool.Content]
301+
let structuredContent: Value?
285302
let isError: Bool?
286303
}
287304

@@ -333,8 +350,14 @@ class ToolCallInfo: ObservableObject, Identifiable {
333350
return nil
334351
}
335352

336-
let (content, isError) = try await client.callTool(name: tool.name, arguments: arguments)
337-
self.result = ToolResult(content: content, isError: isError)
353+
// Use send() directly to get full result including structuredContent
354+
let request = CallTool.request(.init(name: tool.name, arguments: arguments))
355+
let callResult = try await client.send(request)
356+
self.result = ToolResult(
357+
content: callResult.content,
358+
structuredContent: callResult.structuredContent,
359+
isError: callResult.isError
360+
)
338361

339362
if let uiResourceUri = getUiResourceUri(from: tool) {
340363
state = .loadingUi
@@ -407,12 +430,10 @@ class ToolCallInfo: ObservableObject, Identifiable {
407430
}
408431

409432
bridge.onMessage = { role, content in
410-
print("[Host] Message from Guest UI: \(role)")
411433
return McpUiMessageResult(isError: false)
412434
}
413435

414436
bridge.onOpenLink = { url in
415-
print("[Host] Open link request: \(url)")
416437
if let urlObj = URL(string: url) {
417438
await MainActor.run {
418439
UIApplication.shared.open(urlObj)
@@ -422,11 +443,10 @@ class ToolCallInfo: ObservableObject, Identifiable {
422443
}
423444

424445
bridge.onLoggingMessage = { level, data, logger in
425-
print("[Host] Guest UI log [\(level)]: \(data.value)")
446+
// Guest UI logs can be captured here if needed
426447
}
427448

428449
bridge.onSizeChange = { [weak self] width, height in
429-
print("[Host] Size change: \(width ?? 0) x \(height ?? 0)")
430450
if let height = height {
431451
Task { @MainActor in
432452
self?.preferredHeight = CGFloat(height)
@@ -495,10 +515,14 @@ class ToolCallInfo: ObservableObject, Identifiable {
495515
return nil
496516
}
497517
}
498-
let params: [String: AnyCodable] = [
518+
var params: [String: AnyCodable] = [
499519
"content": AnyCodable(contentDicts),
500520
"isError": AnyCodable(result.isError ?? false)
501521
]
522+
// Include structuredContent if present
523+
if let sc = result.structuredContent {
524+
params["structuredContent"] = AnyCodable(sc.toAny())
525+
}
502526
try await bridge.sendToolResult(params)
503527
}
504528

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

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ struct WebViewContainer: UIViewRepresentable {
2020
// Enable JavaScript
2121
configuration.preferences.javaScriptEnabled = true
2222

23+
// Add console log handler
24+
let contentController = configuration.userContentController
25+
contentController.add(context.coordinator, name: "consoleLog")
26+
27+
// Inject console override script
28+
let consoleScript = WKUserScript(source: """
29+
(function() {
30+
var originalLog = console.log;
31+
var originalError = console.error;
32+
var originalWarn = console.warn;
33+
console.log = function() {
34+
originalLog.apply(console, arguments);
35+
window.webkit.messageHandlers.consoleLog.postMessage({level: 'log', args: Array.from(arguments).map(String)});
36+
};
37+
console.error = function() {
38+
originalError.apply(console, arguments);
39+
window.webkit.messageHandlers.consoleLog.postMessage({level: 'error', args: Array.from(arguments).map(String)});
40+
};
41+
console.warn = function() {
42+
originalWarn.apply(console, arguments);
43+
window.webkit.messageHandlers.consoleLog.postMessage({level: 'warn', args: Array.from(arguments).map(String)});
44+
};
45+
})();
46+
""", injectionTime: .atDocumentStart, forMainFrameOnly: false)
47+
contentController.addUserScript(consoleScript)
48+
2349
// Create web view
2450
let webView = WKWebView(frame: .zero, configuration: configuration)
2551
webView.scrollView.isScrollEnabled = true
@@ -104,10 +130,21 @@ struct WebViewContainer: UIViewRepresentable {
104130
Coordinator()
105131
}
106132

107-
/// Coordinator to handle WebView navigation
108-
class Coordinator: NSObject, WKNavigationDelegate {
133+
/// Coordinator to handle WebView navigation and console logging
134+
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
109135
var hasLoadedContent = false
110136

137+
// Handle console log messages from JavaScript
138+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
139+
if message.name == "consoleLog",
140+
let body = message.body as? [String: Any],
141+
let level = body["level"] as? String,
142+
let args = body["args"] as? [String] {
143+
let text = args.joined(separator: " ")
144+
print("[JS \(level.uppercased())] \(text)")
145+
}
146+
}
147+
111148
func webView(
112149
_ webView: WKWebView,
113150
didFinish navigation: WKNavigation!

0 commit comments

Comments
 (0)