Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Example/ClaudeCodeSDKExample/build/
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,33 @@ struct ChatView: View {
@State var viewModel = ChatViewModel(claudeClient: ClaudeCodeClient(debug: true))
@State private var messageText: String = ""
@FocusState private var isTextFieldFocused: Bool
@State private var showingSessions = false
@State private var showingMCPConfig = false

var body: some View {
VStack {
// Top button bar
HStack {
// List sessions button
Button(action: {
viewModel.listSessions()
showingSessions = true
}) {
Image(systemName: "list.bullet.rectangle")
.font(.title2)
}

// MCP Config button
Button(action: {
showingMCPConfig = true
}) {
Image(systemName: "gearshape")
.font(.title2)
.foregroundColor(viewModel.isMCPEnabled ? .green : .primary)
}

Spacer()

Button(action: {
clearChat()
}) {
Expand Down Expand Up @@ -100,6 +121,18 @@ struct ChatView: View {
}
}
.navigationTitle("Claude Code Chat")
.sheet(isPresented: $showingSessions) {
SessionsListView(sessions: viewModel.sessions, isPresented: $showingSessions)
.frame(minWidth: 500, minHeight: 500)
}
.sheet(isPresented: $showingMCPConfig) {
MCPConfigView(
isMCPEnabled: $viewModel.isMCPEnabled,
mcpConfigPath: $viewModel.mcpConfigPath,
isPresented: $showingMCPConfig
)
.frame(minWidth: 500, minHeight: 500)
}
}

private func sendMessage() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// MCPConfigView.swift
// ClaudeCodeSDKExample
//
// Created by Assistant on 6/17/25.
//

import SwiftUI

struct MCPConfigView: View {
@Binding var isMCPEnabled: Bool
@Binding var mcpConfigPath: String
@Binding var isPresented: Bool

var body: some View {
Form {
Section(header: Text("MCP Configuration")) {
Toggle("Enable MCP", isOn: $isMCPEnabled)

if isMCPEnabled {
VStack(alignment: .leading, spacing: 8) {
Text("Config File Path")
.font(.caption)
.foregroundColor(.secondary)

HStack {
TextField("e.g., /path/to/mcp-config.json", text: $mcpConfigPath)
.textFieldStyle(RoundedBorderTextFieldStyle())
.disableAutocorrection(true)

Button("Load Example") {
// Get the absolute path to the example file
let absolutePath = "/Users/jamesrochabrun/Desktop/git/ClaudeCodeSDK/Example/ClaudeCodeSDKExample/mcp-config-example.json"

// Check if file exists at the absolute path
if FileManager.default.fileExists(atPath: absolutePath) {
mcpConfigPath = absolutePath
} else {
// Try to find it relative to the app bundle (for when running from Xcode)
if let bundlePath = Bundle.main.resourcePath {
let bundleExamplePath = "\(bundlePath)/mcp-config-example.json"
if FileManager.default.fileExists(atPath: bundleExamplePath) {
mcpConfigPath = bundleExamplePath
} else {
// Show an error or use a placeholder
mcpConfigPath = "Error: Could not find mcp-config-example.json"
}
}
}
}
.buttonStyle(.bordered)
}
}
}
}

if isMCPEnabled {
Section(header: Text("Example Configuration")) {
Text(exampleConfig)
.font(.system(.caption, design: .monospaced))
.padding(8)
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
}

Section(header: Text("Notes")) {
Text("• MCP tools must be explicitly allowed using allowedTools")
.font(.caption)
Text("• MCP tool names follow the pattern: mcp__<serverName>__<toolName>")
.font(.caption)
Text("• Use mcp__<serverName> to allow all tools from a server")
.font(.caption)
}

Section(header: Text("XcodeBuildMCP Features")) {
Text("The example XcodeBuildMCP server provides tools for:")
.font(.caption)
.fontWeight(.semibold)
Text("• Xcode project management")
.font(.caption)
Text("• iOS/macOS simulator management")
.font(.caption)
Text("• Building and running apps")
.font(.caption)
Text("• Managing provisioning profiles")
.font(.caption)
}
}
}
.navigationTitle("MCP Settings")
.toolbar {
ToolbarItem(placement: .automatic) {
Button("Done") {
isPresented = false
}
}
}
}

private var exampleConfig: String {
"""
{
"mcpServers": {
"XcodeBuildMCP": {
"command": "npx",
"args": [
"-y",
"xcodebuildmcp@latest"
]
}
}
}
"""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//
// SessionsListView.swift
// ClaudeCodeSDKExample
//
// Created by Assistant on 6/17/25.
//

import SwiftUI
import ClaudeCodeSDK

struct SessionsListView: View {
let sessions: [SessionInfo]
@Binding var isPresented: Bool

var body: some View {
NavigationView {
VStack {
if sessions.isEmpty {
Text("No sessions found")
.foregroundColor(.secondary)
.padding()
} else {
List(sessions, id: \.id) { session in
VStack(alignment: .leading, spacing: 4) {
Text(session.id)
.font(.system(.caption, design: .monospaced))
.foregroundColor(.secondary)

if let created = session.created {
Text("Created: \(formattedDate(created))")
.font(.caption)
}

HStack {
if let lastActive = session.lastActive {
Text("Last active: \(formattedDate(lastActive))")
.font(.caption)
}

Spacer()

if let totalCost = session.totalCostUsd {
Text("$\(String(format: "%.4f", totalCost))")
.font(.caption)
.foregroundColor(.blue)
}
}

if let project = session.project {
Text("Project: \(project)")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 4)
}
}
}
.navigationTitle("Sessions")
.toolbar {
ToolbarItem(placement: .automatic) {
Button("Done") {
isPresented = false
}
}
}
}
}

private func formattedDate(_ dateString: String) -> String {
// Try to parse ISO8601 date
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

if let date = formatter.date(from: dateString) {
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .short
displayFormatter.timeStyle = .short
return displayFormatter.string(from: date)
}

return dateString
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ public class ChatViewModel {
/// Error state
public var error: Error?

/// Sessions list
var sessions: [SessionInfo] = []

/// MCP configuration enabled
var isMCPEnabled: Bool = false

/// MCP config path
var mcpConfigPath: String = ""


// MARK: - Initialization

Expand Down Expand Up @@ -99,13 +108,48 @@ public class ChatViewModel {
isLoading = false
}

/// Lists all available sessions
public func listSessions() {
Task {
do {
let fetchedSessions = try await claudeClient.listSessions()
await MainActor.run {
self.sessions = fetchedSessions
logger.info("Fetched \(fetchedSessions.count) sessions")
}
} catch {
await MainActor.run {
self.error = error
logger.error("Failed to list sessions: \(error)")
}
}
}
}

// MARK: - Private Methods

private func startNewConversation(prompt: String, messageId: UUID) async throws {
var options = ClaudeCodeOptions()
options.allowedTools = allowedTools
options.verbose = true

// Add MCP config if enabled
if self.isMCPEnabled && !self.mcpConfigPath.isEmpty {
options.mcpConfigPath = self.mcpConfigPath
// Add MCP tools to allowed tools if using MCP
var updatedAllowedTools = self.allowedTools

// Generate MCP tool patterns from the configuration file
let mcpToolPatterns = MCPToolFormatter.generateAllowedToolPatterns(fromConfigPath: self.mcpConfigPath)
updatedAllowedTools.append(contentsOf: mcpToolPatterns)

options.allowedTools = updatedAllowedTools

self.logger.info("MCP enabled with config: \(self.mcpConfigPath)")
self.logger.info("MCP servers found: \(MCPToolFormatter.extractServerNames(fromConfigPath: self.mcpConfigPath))")
self.logger.info("Allowed tools: \(updatedAllowedTools)")
}

let result = try await claudeClient.runSinglePrompt(
prompt: prompt,
outputFormat: .streamJson,
Expand All @@ -120,6 +164,19 @@ public class ChatViewModel {
options.allowedTools = allowedTools
options.verbose = true

// Add MCP config if enabled
if isMCPEnabled && !mcpConfigPath.isEmpty {
options.mcpConfigPath = mcpConfigPath
// Add MCP tools to allowed tools if using MCP
var updatedAllowedTools = allowedTools

// Generate MCP tool patterns from the configuration file
let mcpToolPatterns = MCPToolFormatter.generateAllowedToolPatterns(fromConfigPath: mcpConfigPath)
updatedAllowedTools.append(contentsOf: mcpToolPatterns)

options.allowedTools = updatedAllowedTools
}

let result = try await claudeClient.resumeConversation(
sessionId: sessionId,
prompt: prompt,
Expand Down Expand Up @@ -158,12 +215,12 @@ public class ChatViewModel {

switch chunk {
case .initSystem(let initMessage):

if currentSessionId == nil { // Only update if not already in a conversation
self.currentSessionId = initMessage.sessionId
logger.debug("Started new session: \(initMessage.sessionId)")
self.currentSessionId = initMessage.sessionId
logger.debug("Started new session: \(initMessage.sessionId)")
} else {
logger.debug("Continuing with new session ID: \(initMessage.sessionId)")
logger.debug("Continuing with new session ID: \(initMessage.sessionId)")
}

case .assistant(let message):
Expand Down Expand Up @@ -365,3 +422,4 @@ extension ContentItem {
return result
}
}

Loading