Skip to content

Commit 18dcffa

Browse files
authored
Merge pull request #357 from atom2ueki/feat/mcp
2 parents e40ecab + 3ca2079 commit 18dcffa

18 files changed

+1299
-75
lines changed

Demo/App/APIProvidedView.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import SwiftUI
1111

1212
struct APIProvidedView: View {
1313
@Binding var apiKey: String
14+
@Binding var githubToken: String
1415
@StateObject var chatStore: ChatStore
1516
@StateObject var imageStore: ImageStore
1617
@StateObject var assistantStore: AssistantStore
1718
@StateObject var miscStore: MiscStore
1819
@StateObject var responsesStore: ResponsesStore
20+
@StateObject var mcpToolsStore: MCPToolsStore
1921

2022
@State var isShowingAPIConfigModal: Bool = true
2123

@@ -24,9 +26,11 @@ struct APIProvidedView: View {
2426

2527
init(
2628
apiKey: Binding<String>,
29+
githubToken: Binding<String>,
2730
idProvider: @escaping () -> String
2831
) {
2932
self._apiKey = apiKey
33+
self._githubToken = githubToken
3034

3135
let client = APIProvidedView.makeClient(apiKey: apiKey.wrappedValue)
3236
self._chatStore = StateObject(
@@ -59,6 +63,9 @@ struct APIProvidedView: View {
5963
).responses
6064
)
6165
)
66+
self._mcpToolsStore = StateObject(
67+
wrappedValue: MCPToolsStore(githubToken: githubToken)
68+
)
6269
}
6370

6471
var body: some View {
@@ -67,8 +74,13 @@ struct APIProvidedView: View {
6774
imageStore: imageStore,
6875
assistantStore: assistantStore,
6976
miscStore: miscStore,
70-
responsesStore: responsesStore
77+
responsesStore: responsesStore,
78+
mcpToolsStore: mcpToolsStore
7179
)
80+
.onAppear {
81+
// Connect MCP tools store to responses store
82+
responsesStore.mcpToolsStore = mcpToolsStore
83+
}
7284
.onChange(of: apiKey) { _, newApiKey in
7385
let client = APIProvidedView.makeClient(apiKey: newApiKey)
7486
chatStore.openAIClient = client

Demo/App/ContentView.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ struct ContentView: View {
1515
@ObservedObject var assistantStore: AssistantStore
1616
@ObservedObject var miscStore: MiscStore
1717
@ObservedObject var responsesStore: ResponsesStore
18+
@ObservedObject var mcpToolsStore: MCPToolsStore
1819

1920
@State private var selectedTab = 0
2021
@Environment(\.idProviderValue) var idProvider
@@ -51,6 +52,12 @@ struct ContentView: View {
5152
}
5253
.tag(3)
5354

55+
MCPToolsView(mcpStore: mcpToolsStore)
56+
.tabItem {
57+
Label("Github MCP", systemImage: "wrench.and.screwdriver")
58+
}
59+
.tag(4)
60+
5461
MiscView(
5562
store: miscStore,
5663
chatStore: chatStore,
@@ -59,7 +66,7 @@ struct ContentView: View {
5966
.tabItem {
6067
Label("Misc", systemImage: "ellipsis")
6168
}
62-
.tag(4)
69+
.tag(5)
6370
}
6471
}
6572
}

Demo/App/DemoApp.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import SwiftUI
1212
@main
1313
struct DemoApp: App {
1414
@AppStorage("apiKey") var apiKey: String = ""
15+
@AppStorage("githubToken") var githubToken: String = ""
1516
@State var isShowingAPIConfigModal: Bool = true
1617

1718
let idProvider: () -> String
@@ -29,6 +30,7 @@ struct DemoApp: App {
2930
Group {
3031
APIProvidedView(
3132
apiKey: $apiKey,
33+
githubToken: $githubToken,
3234
idProvider: idProvider
3335
)
3436
}

Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 37 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Demo/DemoChat/Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,16 @@ let package = Package(
1414
],
1515
dependencies: [
1616
.package(name: "OpenAI", path: "../.."),
17-
.package(url: "https://github.com/exyte/Chat.git", from: "2.5.7")
17+
.package(url: "https://github.com/exyte/Chat.git", from: "2.5.7"),
18+
.package(url: "https://github.com/modelcontextprotocol/swift-sdk.git", from: "0.9.0")
1819
],
1920
targets: [
2021
.target(
2122
name: "DemoChat",
2223
dependencies: [
2324
"OpenAI",
2425
.product(name: "ExyteChat", package: "Chat"),
26+
.product(name: "MCP", package: "swift-sdk")
2527
],
2628
path: "Sources"
2729
),
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//
2+
// MCPToolsStore.swift
3+
// DemoChat
4+
//
5+
// Created by Tony Li on 24/6/25.
6+
//
7+
8+
import Foundation
9+
import MCP
10+
import OpenAI
11+
import Logging
12+
import SwiftUI
13+
14+
@MainActor
15+
public final class MCPToolsStore: ObservableObject {
16+
@Published public var availableTools: [MCPToolInfo] = []
17+
@Published public var enabledTools: Set<String> = []
18+
@Published public var isConnecting: Bool = false
19+
@Published public var connectionError: String?
20+
@Published public var isConnected: Bool = false
21+
22+
public var githubToken: Binding<String>
23+
private var mcpClient: MCP.Client?
24+
private let logger = Logger(label: "mcp.tools.store")
25+
26+
// Persistence keys
27+
private let enabledToolsKey = "mcpEnabledTools"
28+
29+
public struct MCPToolInfo: Identifiable, Hashable {
30+
public let id: String
31+
public let name: String
32+
public let description: String
33+
public let inputSchema: MCP.Value?
34+
35+
public init(from tool: MCP.Tool) {
36+
self.id = tool.name
37+
self.name = tool.name
38+
self.description = tool.description
39+
self.inputSchema = tool.inputSchema
40+
}
41+
}
42+
43+
public init(githubToken: Binding<String>) {
44+
self.githubToken = githubToken
45+
loadEnabledTools()
46+
47+
// Auto-connect if token is available
48+
if !githubToken.wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
49+
Task {
50+
await connectToGitHubMCP()
51+
}
52+
}
53+
}
54+
55+
/// Connect to GitHub MCP server and discover available tools
56+
public func connectToGitHubMCP() async {
57+
isConnecting = true
58+
connectionError = nil
59+
60+
// Check if GitHub token is provided
61+
guard !githubToken.wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
62+
connectionError = "GitHub token is required for authentication"
63+
isConnecting = false
64+
return
65+
}
66+
67+
do {
68+
// Create MCP client
69+
let client = MCP.Client(
70+
name: "OpenAI-Demo",
71+
version: "1.0.0"
72+
)
73+
74+
// Create HTTP transport for GitHub MCP server with authentication headers
75+
let configuration = URLSessionConfiguration.default
76+
configuration.httpAdditionalHeaders = [
77+
"Authorization": "Bearer \(githubToken.wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines))"
78+
]
79+
80+
let transport = HTTPClientTransport(
81+
endpoint: URL(string: "https://api.githubcopilot.com/mcp/")!,
82+
configuration: configuration,
83+
streaming: true,
84+
logger: logger
85+
)
86+
87+
// Connect to the server
88+
let result = try await client.connect(transport: transport)
89+
90+
// Check if server supports tools
91+
guard result.capabilities.tools != nil else {
92+
throw MCPError.invalidRequest("GitHub MCP server does not support tools")
93+
}
94+
95+
// List available tools
96+
let toolsResponse = try await client.listTools()
97+
98+
// Update UI
99+
self.mcpClient = client
100+
self.availableTools = toolsResponse.tools.map { MCPToolInfo(from: $0) }
101+
self.isConnected = true
102+
103+
// Restore enabled tools that are still available
104+
let availableToolNames = Set(self.availableTools.map { $0.name })
105+
self.enabledTools = self.enabledTools.intersection(availableToolNames)
106+
107+
logger.info("Successfully connected to GitHub MCP server with \(toolsResponse.tools.count) tools")
108+
109+
} catch {
110+
connectionError = error.localizedDescription
111+
logger.error("Failed to connect to GitHub MCP server: \(error)")
112+
}
113+
114+
isConnecting = false
115+
}
116+
117+
/// Disconnect from MCP server
118+
public func disconnect() async {
119+
if let client = mcpClient {
120+
await client.disconnect()
121+
mcpClient = nil
122+
}
123+
124+
isConnected = false
125+
availableTools = []
126+
enabledTools = []
127+
connectionError = nil
128+
129+
// Clear the GitHub token from storage
130+
githubToken.wrappedValue = ""
131+
132+
// Clear saved enabled tools
133+
UserDefaults.standard.removeObject(forKey: enabledToolsKey)
134+
}
135+
136+
/// Toggle tool enabled state
137+
public func toggleTool(_ toolName: String) {
138+
if enabledTools.contains(toolName) {
139+
enabledTools.remove(toolName)
140+
} else {
141+
enabledTools.insert(toolName)
142+
}
143+
saveEnabledTools()
144+
}
145+
146+
/// Enable all available tools
147+
public func enableAllTools() {
148+
enabledTools = Set(availableTools.map { $0.name })
149+
saveEnabledTools()
150+
}
151+
152+
/// Disable all tools
153+
public func disableAllTools() {
154+
enabledTools.removeAll()
155+
saveEnabledTools()
156+
}
157+
158+
/// Check if all available tools are enabled
159+
public var areAllToolsEnabled: Bool {
160+
!availableTools.isEmpty && enabledTools.count == availableTools.count
161+
}
162+
163+
/// Get list of enabled tool names for OpenAI API
164+
public var allowedTools: Components.Schemas.MCPTool.AllowedToolsPayload? {
165+
guard !enabledTools.isEmpty else { return nil }
166+
return .case1(Array(enabledTools))
167+
}
168+
169+
/// Get authentication headers for GitHub MCP server
170+
public var authHeaders: [String: String]? {
171+
guard !githubToken.wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
172+
return nil
173+
}
174+
return ["Authorization": "Bearer \(githubToken.wrappedValue.trimmingCharacters(in: .whitespacesAndNewlines))"]
175+
}
176+
177+
// MARK: - Persistence
178+
179+
private func loadEnabledTools() {
180+
if let savedTools = UserDefaults.standard.array(forKey: enabledToolsKey) as? [String] {
181+
enabledTools = Set(savedTools)
182+
}
183+
}
184+
185+
private func saveEnabledTools() {
186+
UserDefaults.standard.set(Array(enabledTools), forKey: enabledToolsKey)
187+
}
188+
189+
}

0 commit comments

Comments
 (0)