-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathMCPRegistryURLView.swift
More file actions
239 lines (217 loc) · 9.95 KB
/
MCPRegistryURLView.swift
File metadata and controls
239 lines (217 loc) · 9.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
import AppKit
import Logger
import SharedUIComponents
import SwiftUI
import Client
import XPCShared
import GitHubCopilotService
import ComposableArchitecture
struct MCPRegistryURLView: View {
@State private var isExpanded: Bool = false
@AppStorage(\.mcpRegistryURL) var mcpRegistryURL
@AppStorage(\.mcpRegistryURLHistory) private var mcpRegistryURLHistory
@State private var isLoading: Bool = false
@State private var tempURLText: String = ""
@State private var errorMessage: String = ""
@State private var mcpRegistry: [MCPRegistryEntry]? = nil
private let maxURLLength = 2048
private let mcpRegistryUrlVersion = "/v0/servers"
var body: some View {
WithPerceptionTracking {
VStack(spacing: 0) {
DisclosureSettingsRow(
isExpanded: $isExpanded,
accessibilityLabel: { $0 ? "Collapse mcp registry URL section" : "Expand mcp registry URL section" },
title: { Text("MCP Registry URL").font(.headline) + Text(" (Optional)") },
subtitle: { Text("Connect to available MCP servers for your AI workflows using the Registry URL.") },
actions: {
HStack(spacing: 8) {
if isLoading {
ProgressView().controlSize(.small)
}
Button {
isExpanded = true
} label: {
HStack(spacing: 0) {
Image(systemName: "square.and.pencil")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 12, height: 12, alignment: .center)
.padding(4)
Text("Edit URL")
}
.conditionalFontWeight(.semibold)
}
.buttonStyle(.bordered)
.help("Configure your MCP Registry URL")
.disabled(mcpRegistry?.first?.registryAccess == .registryOnly)
Button { Task{ await loadMCPServers() } } label: {
HStack(spacing: 0) {
Image(systemName: "square.grid.2x2")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 12, height: 12, alignment: .center)
.padding(4)
Text("Browse MCP Servers...")
}
.conditionalFontWeight(.semibold)
}
.buttonStyle(.bordered)
.help("Browse MCP Servers")
}
.padding(.vertical, 12)
}
)
if isExpanded {
VStack(alignment: .leading, spacing: 8) {
MCPRegistryURLInputField(
urlText: $tempURLText,
maxURLLength: maxURLLength,
isSheet: false,
mcpRegistryEntry: mcpRegistry?.first,
onValidationChange: { _ in
// Only validate, don't update mcpRegistryURL here
},
onCommit: {
// Update mcpRegistryURL when user presses Enter
if tempURLText != mcpRegistryURL {
mcpRegistryURL = tempURLText
}
}
)
if !errorMessage.isEmpty {
Badge(text: errorMessage, level: .danger, icon: "xmark.circle.fill")
}
}
.padding(.leading, 36)
.padding([.trailing, .bottom], 20)
.background(QuaternarySystemFillColor.opacity(0.75))
.transition(.opacity.combined(with: .scale(scale: 1, anchor: .top)))
.onAppear {
tempURLText = mcpRegistryURL
}
}
}
.cornerRadius(12)
.clipShape(RoundedRectangle(cornerRadius: 12))
.overlay(
RoundedRectangle(cornerRadius: 12)
.inset(by: 0.5)
.stroke(SecondarySystemFillColor, lineWidth: 1)
.animation(.easeInOut(duration: 0.3), value: isExpanded)
)
.animation(.easeInOut(duration: 0.3), value: isExpanded)
.onAppear {
tempURLText = mcpRegistryURL
Task { await getMCPRegistryAllowlist() }
}
.onReceive(DistributedNotificationCenter.default().publisher(for: .authStatusDidChange)) { _ in
Task { await getMCPRegistryAllowlist() }
}
.onChange(of: mcpRegistryURL) { newValue in
// Update the temp text to reflect the new URL
tempURLText = newValue
Task { await updateGalleryWindowIfOpen() }
}
.onChange(of: mcpRegistry) { _ in
Task { await updateGalleryWindowIfOpen() }
}
}
}
private func loadMCPServers() async {
// Update mcpRegistryURL with current tempURLText before loading
if tempURLText != mcpRegistryURL {
mcpRegistryURL = tempURLText
}
isLoading = true
defer { isLoading = false }
do {
let service = try getService()
let serverList = try await service.listMCPRegistryServers(
.init(baseUrl: mcpRegistryURL, limit: 30)
)
guard let serverList = serverList, !serverList.servers.isEmpty else {
Logger.client.info("No MCP servers found at registry URL: \(mcpRegistryURL)")
return
}
// Add to history on successful load
mcpRegistryURLHistory.addToHistory(mcpRegistryURL)
errorMessage = ""
MCPServerGalleryWindow.open(serverList: serverList, mcpRegistryEntry: mcpRegistry?.first)
} catch {
Logger.client.error("Failed to load MCP servers from registry: \(error.localizedDescription)")
if let serviceError = error as? XPCExtensionServiceError {
errorMessage = serviceError.underlyingError?.localizedDescription ?? serviceError.localizedDescription
} else {
errorMessage = error.localizedDescription
}
isExpanded = true
}
}
private func getMCPRegistryAllowlist() async {
isLoading = true
defer { isLoading = false }
do {
let service = try getService()
// Only fetch allowlist if user is logged in
let authStatus = try await service.getXPCServiceAuthStatus()
guard authStatus?.status == .loggedIn else {
Logger.client.info("User not logged in, skipping MCP registry allowlist fetch")
return
}
let result = try await service.getMCPRegistryAllowlist()
guard let result = result, !result.mcpRegistries.isEmpty else {
if result == nil {
Logger.client.error("Failed to get allowlist result")
} else {
mcpRegistry = []
}
return
}
if let firstRegistry = result.mcpRegistries.first {
let baseUrl = firstRegistry.url.hasSuffix("/")
? String(firstRegistry.url.dropLast())
: firstRegistry.url
let entry = MCPRegistryEntry(
url: baseUrl + mcpRegistryUrlVersion,
registryAccess: firstRegistry.registryAccess,
owner: firstRegistry.owner
)
mcpRegistry = [entry]
Logger.client.info("Current MCP Registry Entry: \(entry)")
// If registryOnly, force the URL to be the registry URL
if entry.registryAccess == .registryOnly {
mcpRegistryURL = entry.url
tempURLText = entry.url
}
}
} catch {
Logger.client.error("Failed to get MCP allowlist from registry: \(error)")
}
}
private func updateGalleryWindowIfOpen() async {
// Only update if the gallery window is currently open
guard MCPServerGalleryWindow.isOpen() else {
return
}
isLoading = true
defer { isLoading = false }
// Let the view model handle the entire update flow including clearing and fetching
if let error = await MCPServerGalleryWindow.refreshFromURL(mcpRegistryEntry: mcpRegistry?.first) {
// Display error in the URL view
if let serviceError = error as? XPCExtensionServiceError {
errorMessage = serviceError.underlyingError?.localizedDescription ?? serviceError.localizedDescription
} else {
errorMessage = error.localizedDescription
}
isExpanded = true
} else {
errorMessage = ""
}
}
}
#Preview {
MCPRegistryURLView()
.padding()
.frame(width: 900)
}