Skip to content

Commit 2a6a8d6

Browse files
committed
Pre-release 0.44.146
1 parent 1d98c6e commit 2a6a8d6

24 files changed

+638
-201
lines changed

Core/Sources/HostApp/SharedComponents/CardGroupBoxStyle.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public struct CardGroupBoxStyle: GroupBoxStyle {
1515
configuration.label.foregroundColor(.primary)
1616
configuration.content.foregroundColor(.primary)
1717
}
18-
.padding(8)
18+
.padding(.vertical, 12)
19+
.padding(.horizontal, 20)
1920
.frame(maxWidth: .infinity, alignment: .topLeading)
2021
.background(backgroundColor)
2122
.cornerRadius(12)

Core/Sources/HostApp/ToolsConfigView.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ struct MCPConfigView: View {
7171
setupConfigFilePath()
7272
Task {
7373
await updateFeatureFlag()
74+
// Start monitoring if feature is already enabled on initial load
75+
if isMCPFFEnabled {
76+
startMonitoringConfigFile()
77+
}
7478
}
7579
}
7680
.onDisappear {
@@ -179,18 +183,22 @@ struct MCPConfigView: View {
179183
stopMonitoringConfigFile() // Stop existing monitoring if any
180184

181185
isMonitoring = true
186+
Logger.client.info("Starting MCP config file monitoring")
182187

183188
fileMonitorTask = Task {
184189
let configFileURL = URL(fileURLWithPath: configFilePath)
185190

186191
// Check for file changes periodically
187192
while isMonitoring {
188-
try? await Task.sleep(nanoseconds: 3_000_000_000) // Check every 3 seconds
193+
try? await Task.sleep(nanoseconds: 3_000_000_000) // Check every 1 second for better responsiveness
194+
195+
guard isMonitoring else { break } // Extra check after sleep
189196

190197
let currentDate = getFileModificationDate(url: configFileURL)
191198

192199
if let currentDate = currentDate, currentDate != lastModificationDate {
193200
// File modification date has changed, update our record
201+
Logger.client.info("MCP config file change detected")
194202
lastModificationDate = currentDate
195203

196204
// Read and validate the updated content
@@ -204,14 +212,18 @@ struct MCPConfigView: View {
204212
// If JSON is invalid, show error
205213
await MainActor.run {
206214
toast("Invalid JSON in MCP configuration file", .error)
215+
Logger.client.info("Invalid JSON detected during monitoring")
207216
}
208217
}
209218
}
210219
}
220+
Logger.client.info("Stopped MCP config file monitoring")
211221
}
212222
}
213223

214224
private func stopMonitoringConfigFile() {
225+
guard isMonitoring else { return }
226+
Logger.client.info("Stopping MCP config file monitoring")
215227
isMonitoring = false
216228
fileMonitorTask?.cancel()
217229
fileMonitorTask = nil

Core/Sources/HostApp/ToolsSettings/CopilotMCPToolManagerObservable.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class CopilotMCPToolManagerObservable: ObservableObject {
1717
.receive(on: DispatchQueue.main)
1818
.sink { [weak self] _ in
1919
guard let self = self else { return }
20+
Logger.client.info("MCP tools change notification received")
2021
Task {
2122
await self.refreshMCPServerTools()
2223
}
@@ -31,6 +32,7 @@ class CopilotMCPToolManagerObservable: ObservableObject {
3132

3233
@MainActor
3334
private func refreshMCPServerTools() async {
35+
Logger.client.info("Refreshing MCP server tools...")
3436
do {
3537
let service = try getService()
3638
let mcpTools = try await service.getAvailableMCPServerToolsCollections()
@@ -43,9 +45,14 @@ class CopilotMCPToolManagerObservable: ObservableObject {
4345
private func refreshTools(tools: [MCPServerToolsCollection]?) {
4446
guard let tools = tools else {
4547
// nil means the tools data is ready, and skip it first.
48+
Logger.client.info("MCP tools data not ready yet, skipping refresh")
4649
return
4750
}
4851

52+
let totalToolsCount = tools.reduce(0) { $0 + $1.tools.count }
53+
let serverNames = tools.map { $0.name }.joined(separator: ", ")
54+
Logger.client.info("Refreshed MCP tools - Servers: \(tools.count), Total tools: \(totalToolsCount), Server names: [\(serverNames)]")
55+
4956
AppState.shared.cleanupMCPToolsStatus(availableTools: tools)
5057
AppState.shared.createMCPToolsStatus(tools)
5158
self.availableMCPServerTools = tools

Core/Sources/HostApp/ToolsSettings/MCPAppState.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,10 @@ extension AppState {
9595

9696
// Get all available server names and their respective tool names
9797
let availableServerMap = Dictionary(
98-
uniqueKeysWithValues: availableTools.map { collection in
98+
availableTools.map { collection in
9999
(collection.name, Set(collection.tools.map { $0.name }))
100100
}
101-
)
101+
) { first, _ in first }
102102

103103
// Remove servers that don't exist in available tools
104104
existingServers.removeAll { !availableServerMap.keys.contains($0.name) }

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryInstallation.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public class MCPRegistryService: ObservableObject {
173173

174174
// Add headers if present
175175
if let headers = remote.headers, !headers.isEmpty {
176-
let headersDict = Dictionary(uniqueKeysWithValues: headers.map { ($0.name, $0.value ?? "") })
176+
let headersDict = Dictionary(headers.map { ($0.name, $0.value ?? "") }) { first, _ in first }
177177
config["requestInit"] = ["headers": headersDict]
178178
}
179179

@@ -208,7 +208,7 @@ public class MCPRegistryService: ObservableObject {
208208

209209
// Environment variables
210210
if let envVars = package.environmentVariables, !envVars.isEmpty {
211-
config["env"] = Dictionary(uniqueKeysWithValues: envVars.map { ($0.name, $0.value ?? "") })
211+
config["env"] = Dictionary(envVars.map { ($0.name, $0.value ?? "") }) { first, _ in first }
212212
}
213213

214214
addMetadata(to: &config, serverDetail: serverDetail)
@@ -344,10 +344,22 @@ public class MCPRegistryService: ObservableObject {
344344
}
345345

346346
public func createRegistryServerKey(registryURL: String, serverId: String) -> String {
347-
return "\(registryURL)|\(serverId)"
347+
let baseURL = normalizeRegistryURL(registryURL)
348+
return "\(baseURL)|\(serverId)"
348349
}
349350

350351
// MARK: - Registry Key Helpers
352+
353+
private func normalizeRegistryURL(_ url: String) -> String {
354+
// Remove trailing /v0/servers, /v0.1/servers or similar version paths
355+
var normalized = url.trimmingCharacters(in: .whitespacesAndNewlines)
356+
if let range = normalized.range(of: "/v\\d+(\\.\\d+)?/servers$", options: .regularExpression) {
357+
normalized = String(normalized[..<range.lowerBound])
358+
}
359+
// Remove trailing slash
360+
return normalized.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
361+
}
362+
351363
private func expectedRegistryKey(for serverDetail: MCPRegistryServerDetail) -> String? {
352364
guard let serverId = Self.getServerId(from: serverDetail),
353365
let registryURL = try? getRegistryURL() else { return nil }

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLInputField.swift

Lines changed: 76 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,78 +11,115 @@ struct MCPRegistryURLInputField: View {
1111
let defaultMCPRegistryURL = "https://api.mcp.github.com/2025-09-15/v0/servers"
1212
let maxURLLength: Int
1313
let isSheet: Bool
14+
let mcpRegistryEntry: MCPRegistryEntry?
1415
let onValidationChange: ((Bool) -> Void)?
1516

17+
private var isRegistryOnly: Bool {
18+
mcpRegistryEntry?.registryAccess == .registryOnly
19+
}
20+
1621
init(
1722
urlText: Binding<String>,
1823
maxURLLength: Int = 2048,
1924
isSheet: Bool = false,
25+
mcpRegistryEntry: MCPRegistryEntry? = nil,
2026
onValidationChange: ((Bool) -> Void)? = nil
2127
) {
2228
self._urlText = urlText
2329
self.maxURLLength = maxURLLength
2430
self.isSheet = isSheet
31+
self.mcpRegistryEntry = mcpRegistryEntry
2532
self.onValidationChange = onValidationChange
2633
}
2734

2835
var body: some View {
29-
HStack(spacing: 8) {
30-
if isSheet {
31-
TextFieldsContainer {
32-
TextField("MCP Registry URL", text: $urlText)
36+
VStack(alignment: .leading, spacing: 8) {
37+
HStack(spacing: 8) {
38+
if isSheet {
39+
TextFieldsContainer {
40+
TextField("MCP Registry URL", text: $urlText)
41+
.focused($isFocused)
42+
.disabled(isRegistryOnly)
43+
.onChange(of: urlText) { newValue in
44+
handleURLChange(newValue)
45+
}
46+
}
47+
} else {
48+
TextField("MCP Registry URL:", text: $urlText)
49+
.textFieldStyle(.roundedBorder)
3350
.focused($isFocused)
51+
.disabled(isRegistryOnly)
3452
.onChange(of: urlText) { newValue in
3553
handleURLChange(newValue)
3654
}
3755
}
38-
} else {
39-
TextField("MCP Registry URL:", text: $urlText)
40-
.textFieldStyle(.roundedBorder)
41-
.focused($isFocused)
42-
.onChange(of: urlText) { newValue in
43-
handleURLChange(newValue)
56+
57+
Menu {
58+
ForEach(urlHistory, id: \.self) { url in
59+
Button(url) {
60+
urlText = url
61+
isFocused = false
62+
}
4463
}
45-
}
46-
47-
Menu {
48-
ForEach(urlHistory, id: \.self) { url in
49-
Button(url) {
50-
urlText = url
51-
isFocused = false
64+
65+
Divider()
66+
67+
Button("Reset to Default") {
68+
urlText = defaultMCPRegistryURL
5269
}
53-
}
54-
55-
Divider()
56-
57-
Button("Reset to Default") {
58-
urlText = defaultMCPRegistryURL
59-
}
60-
61-
if !urlHistory.isEmpty {
62-
Button("Clear History") {
63-
urlHistory = []
70+
71+
if !urlHistory.isEmpty {
72+
Button("Clear History") {
73+
urlHistory = []
74+
}
6475
}
65-
}
66-
} label: {
67-
Image(systemName: "chevron.down")
76+
} label: {
77+
Image(systemName: "chevron.down")
6878
.resizable()
6979
.scaledToFit()
7080
.frame(width: 11, height: 11)
7181
.padding(isSheet ? 9 : 3)
82+
}
83+
.labelStyle(.iconOnly)
84+
.menuIndicator(.hidden)
85+
.buttonStyle(
86+
HoverButtonStyle(
87+
hoverColor: SecondarySystemFillColor,
88+
backgroundColor: SecondarySystemFillColor,
89+
cornerRadius: isSheet ? 12 : 6
90+
)
91+
)
92+
.opacity(isRegistryOnly ? 0.5 : 1)
93+
.disabled(isRegistryOnly)
7294
}
73-
.labelStyle(.iconOnly)
74-
.menuIndicator(.hidden)
75-
.buttonStyle(
76-
HoverButtonStyle(
77-
hoverColor: SecondarySystemFillColor,
78-
backgroundColor: SecondarySystemFillColor,
79-
cornerRadius: isSheet ? 12 : 6
95+
96+
if isRegistryOnly {
97+
Badge(
98+
text: "This URL is managed by \(mcpRegistryEntry!.owner.login) and cannot be modified",
99+
level: .info,
100+
icon: "info.circle.fill"
80101
)
81-
)
102+
}
103+
}
104+
.onAppear {
105+
if isRegistryOnly, let entryURL = mcpRegistryEntry?.url {
106+
urlText = entryURL
107+
}
108+
}
109+
.onChange(of: mcpRegistryEntry) { newEntry in
110+
if newEntry?.registryAccess == .registryOnly, let entryURL = newEntry?.url {
111+
urlText = entryURL
112+
}
82113
}
83114
}
84115

85116
private func handleURLChange(_ newValue: String) {
117+
// If registryOnly, force the URL back to the registry entry URL
118+
if isRegistryOnly, let entryURL = mcpRegistryEntry?.url {
119+
urlText = entryURL
120+
return
121+
}
122+
86123
let limitedText = String(newValue.prefix(maxURLLength))
87124
if limitedText != newValue {
88125
urlText = limitedText

Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLSheet.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ struct MCPRegistryURLSheet: View {
77
@Environment(\.dismiss) private var dismiss
88
@State private var originalMcpRegistryURL: String = ""
99
@State private var isFormValid: Bool = true
10-
10+
11+
let mcpRegistryEntry: MCPRegistryEntry?
1112
let onURLUpdated: (() -> Void)?
1213

13-
init(onURLUpdated: (() -> Void)? = nil) {
14+
init(mcpRegistryEntry: MCPRegistryEntry? = nil, onURLUpdated: (() -> Void)? = nil) {
15+
self.mcpRegistryEntry = mcpRegistryEntry
1416
self.onURLUpdated = onURLUpdated
1517
}
1618

@@ -28,6 +30,7 @@ struct MCPRegistryURLSheet: View {
2830
MCPRegistryURLInputField(
2931
urlText: $originalMcpRegistryURL,
3032
isSheet: true,
33+
mcpRegistryEntry: mcpRegistryEntry,
3134
onValidationChange: { isValid in
3235
isFormValid = isValid
3336
}
@@ -46,7 +49,7 @@ struct MCPRegistryURLSheet: View {
4649
dismiss()
4750
}
4851
.buttonStyle(.borderedProminent)
49-
.disabled(!isFormValid)
52+
.disabled(!isFormValid || mcpRegistryEntry?.registryAccess == .registryOnly)
5053
}
5154
}
5255
.textFieldStyle(.plain)

0 commit comments

Comments
 (0)