diff --git a/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryInstallation.swift b/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryInstallation.swift index d3ebbc0c..52b1264b 100644 --- a/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryInstallation.swift +++ b/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryInstallation.swift @@ -90,9 +90,55 @@ public class MCPRegistryService: ObservableObject { public static let shared = MCPRegistryService() public static let apiVersion = "v0.1" @AppStorage(\.mcpRegistryBaseURL) var mcpRegistryBaseURL - + @Published public private(set) var mcpRegistryEntries: [MCPRegistryEntry]? + private init() {} + /// Fetches the MCP registry allowlist from the language server and updates + /// ``mcpRegistryEntries``. Safe to call from any view's `onAppear` – + /// duplicate in-flight calls are coalesced via the `isRefreshing` flag. + private var isRefreshing = false + + public func refreshAllowlist() async { + guard !isRefreshing else { return } + isRefreshing = true + defer { isRefreshing = false } + + do { + let service = try getService() + + let authStatus = try await service.getXPCServiceAuthStatus() + guard authStatus?.status == .loggedIn else { + Logger.client.info("User not logged in, skipping MCP registry allowlist fetch") + mcpRegistryEntries = nil + 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 { + mcpRegistryEntries = [] + } + return + } + + if let firstRegistry = result.mcpRegistries.first { + let entry = MCPRegistryEntry( + url: firstRegistry.url, + registryAccess: firstRegistry.registryAccess, + owner: firstRegistry.owner + ) + mcpRegistryEntries = [entry] + Logger.client.info("Current MCP Registry Entry: \(entry)") + } + } catch { + Logger.client.error("Failed to get MCP allowlist from registry: \(error)") + } + } + public static func getServerName(from serverDetail: MCPRegistryServerDetail) -> String { return serverDetail.name } diff --git a/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLView.swift b/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLView.swift index b3cb3537..e01b90c5 100644 --- a/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLView.swift +++ b/Core/Sources/HostApp/ToolsSettings/MCPRegistry/MCPRegistryURLView.swift @@ -14,7 +14,7 @@ struct MCPRegistryURLView: View { @State private var isLoading: Bool = false @State private var tempURLText: String = "" @State private var errorMessage: String = "" - @State private var mcpRegistry: [MCPRegistryEntry]? = nil + @ObservedObject private var registryService = MCPRegistryService.shared private let maxURLLength = 2048 private let mcpRegistryUrlVersion = "/v0.1/servers" @@ -48,7 +48,7 @@ struct MCPRegistryURLView: View { } .buttonStyle(.bordered) .help("Configure your MCP Registry Base URL") - .disabled(mcpRegistry?.first?.registryAccess == .registryOnly) + .disabled(registryService.mcpRegistryEntries?.first?.registryAccess == .registryOnly) Button { Task{ await loadMCPServers() } } label: { HStack(spacing: 0) { @@ -74,7 +74,7 @@ struct MCPRegistryURLView: View { urlText: $tempURLText, maxURLLength: maxURLLength, isSheet: false, - mcpRegistryEntry: mcpRegistry?.first, + mcpRegistryEntry: registryService.mcpRegistryEntries?.first, onValidationChange: { _ in // Only validate, don't update mcpRegistryURL here }, @@ -115,7 +115,7 @@ struct MCPRegistryURLView: View { tempURLText = newValue Task { await updateGalleryWindowIfOpen() } } - .onChange(of: mcpRegistry) { _ in + .onChange(of: registryService.mcpRegistryEntries) { _ in Task { await updateGalleryWindowIfOpen() } } } @@ -145,7 +145,7 @@ struct MCPRegistryURLView: View { mcpRegistryBaseURLHistory.addToHistory(mcpRegistryBaseURL) errorMessage = "" - MCPServerGalleryWindow.open(serverList: serverList, mcpRegistryEntry: mcpRegistry?.first) + MCPServerGalleryWindow.open(serverList: serverList, mcpRegistryEntry: registryService.mcpRegistryEntries?.first) } catch { Logger.client.error("Failed to load MCP servers from registry: \(error.localizedDescription)") if let serviceError = error as? XPCExtensionServiceError { @@ -160,44 +160,14 @@ struct MCPRegistryURLView: View { 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 entry = MCPRegistryEntry( - url: firstRegistry.url, - 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 { - mcpRegistryBaseURL = entry.url - tempURLText = entry.url - } - } - } catch { - Logger.client.error("Failed to get MCP allowlist from registry: \(error)") + + await registryService.refreshAllowlist() + + // If registryOnly, force the URL to be the registry URL + if let entry = registryService.mcpRegistryEntries?.first, + entry.registryAccess == .registryOnly { + mcpRegistryBaseURL = entry.url + tempURLText = entry.url } } @@ -211,7 +181,7 @@ struct MCPRegistryURLView: View { 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) { + if let error = await MCPServerGalleryWindow.refreshFromURL(mcpRegistryEntry: registryService.mcpRegistryEntries?.first) { // Display error in the URL view if let serviceError = error as? XPCExtensionServiceError { errorMessage = serviceError.underlyingError?.localizedDescription ?? serviceError.localizedDescription diff --git a/Core/Sources/HostApp/ToolsSettings/MCPXcodeServerInstallView.swift b/Core/Sources/HostApp/ToolsSettings/MCPXcodeServerInstallView.swift index 3727111d..ccf83061 100644 --- a/Core/Sources/HostApp/ToolsSettings/MCPXcodeServerInstallView.swift +++ b/Core/Sources/HostApp/ToolsSettings/MCPXcodeServerInstallView.swift @@ -13,6 +13,7 @@ struct MCPXcodeServerInstallView: View { /// Cached to avoid repeated file I/O during SwiftUI rendering. @State private var configuredXcodeServerNames: Set = [] @ObservedObject private var mcpToolManager = CopilotMCPToolManagerObservable.shared + @ObservedObject private var registryService = MCPRegistryService.shared private let requiredXcodeVersion = "26.4" private let serverName = "xcode" @@ -39,6 +40,10 @@ struct MCPXcodeServerInstallView: View { isConfigured || isConnected } + private var isRegistryOnly: Bool { + registryService.mcpRegistryEntries?.first?.registryAccess == .registryOnly + } + var body: some View { HStack(alignment: .center, spacing: 16) { VStack(alignment: .leading, spacing: 0) { @@ -61,6 +66,7 @@ struct MCPXcodeServerInstallView: View { .settingsContainerStyle(isExpanded: false) .onAppear { checkInstallationStatus() + Task { await registryService.refreshAllowlist() } } .onChange(of: mcpToolManager.availableMCPServerTools) { _ in checkInstallationStatus() @@ -76,11 +82,13 @@ struct MCPXcodeServerInstallView: View { Text("Requires Xcode \(requiredXcodeVersion) or later. Current version: \(versionText).") } else if isConnected { Text("Xcode's built-in MCP server is connected, enabling richer editor integration.") + } else if isRegistryOnly { + Text("Manual installation of Xcode's built-in MCP server is blocked by your organization's registry policy. Please check the MCP Registry for an approved installation option, or contact your enterprise IT administrator.") } else if isConfiguredButNotConnected { Text("Please confirm in Xcode to allow the built-in MCP server.") } else { VStack(alignment: .leading, spacing: 4) { - Text("Connect Copilot to Xcode’s built‑in MCP server to enable richer editor integration.") + Text("Connect Copilot to Xcode's built-in MCP server to enable richer editor integration.") if let installError { Text(installError) .font(.caption) @@ -96,6 +104,8 @@ struct MCPXcodeServerInstallView: View { EmptyView() } else if isConnected { Text("Connected").foregroundColor(.secondary) + } else if isRegistryOnly { + EmptyView() } else if isConfiguredButNotConnected { HStack(spacing: 6) { ProgressView() diff --git a/Server/package-lock.json b/Server/package-lock.json index 855a73b1..224f0112 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -8,9 +8,9 @@ "name": "@github/copilot-xcode", "version": "0.0.1", "dependencies": { - "@github/copilot-language-server": "1.451.0", - "@github/copilot-language-server-darwin-arm64": "1.451.0", - "@github/copilot-language-server-darwin-x64": "1.451.0", + "@github/copilot-language-server": "1.457.0", + "@github/copilot-language-server-darwin-arm64": "1.457.0", + "@github/copilot-language-server-darwin-x64": "1.457.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "monaco-editor": "0.52.2" @@ -38,9 +38,9 @@ } }, "node_modules/@github/copilot-language-server": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.451.0.tgz", - "integrity": "sha512-ApkyyC0yz1tx+9Yb17SjG0/jpmIgl3H1EO744Thyg+sCt6AsonJMoNTVUPcx0YxEzzK0HafUWeA/4nacTwnTYg==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server/-/copilot-language-server-1.457.0.tgz", + "integrity": "sha512-P+hNX0zPN5+B6zgXPhR6QmUpofV9j9ZswSVxatOKPlaB5KKwGbmkzvrxUPXBRu0eMXCEqOOqtEJ/HBwReaUhkg==", "license": "MIT", "dependencies": { "vscode-languageserver-protocol": "^3.17.5" @@ -49,18 +49,18 @@ "copilot-language-server": "dist/language-server.js" }, "optionalDependencies": { - "@github/copilot-language-server-darwin-arm64": "1.451.0", - "@github/copilot-language-server-darwin-x64": "1.451.0", - "@github/copilot-language-server-linux-arm64": "1.451.0", - "@github/copilot-language-server-linux-x64": "1.451.0", - "@github/copilot-language-server-win32-arm64": "1.451.0", - "@github/copilot-language-server-win32-x64": "1.451.0" + "@github/copilot-language-server-darwin-arm64": "1.457.0", + "@github/copilot-language-server-darwin-x64": "1.457.0", + "@github/copilot-language-server-linux-arm64": "1.457.0", + "@github/copilot-language-server-linux-x64": "1.457.0", + "@github/copilot-language-server-win32-arm64": "1.457.0", + "@github/copilot-language-server-win32-x64": "1.457.0" } }, "node_modules/@github/copilot-language-server-darwin-arm64": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.451.0.tgz", - "integrity": "sha512-aq0dv9oKLt2Y87oEnnsUCWJ0x2qc+t+nyzg3GoT2M6eWr1YrqIL6VlGlmmNB/WWvTSp3w94xy5H6kDpD7rzWgQ==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-arm64/-/copilot-language-server-darwin-arm64-1.457.0.tgz", + "integrity": "sha512-mzeKomqU9NZswkGe6LxMrpfm+jUDBV5i6Al+6HRXkEzxsyOdY7FksqAIspwmqzpiohZ9ObwxUbp7RpcQY0wJCw==", "cpu": [ "arm64" ], @@ -70,9 +70,9 @@ ] }, "node_modules/@github/copilot-language-server-darwin-x64": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.451.0.tgz", - "integrity": "sha512-GX1Fkl84Bh1EpnNYQpexirKrIxgtpUU4iYh58b865dAv7TBpKIyXxP1rSl/2/MCWDV6VuPWYhv5OfzHuiFgacA==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-darwin-x64/-/copilot-language-server-darwin-x64-1.457.0.tgz", + "integrity": "sha512-CTge6InxrUFbWj3bik5jYfUjhqr08Za37an/aAHuTRp++DvoEjIl9eoJpQjdeu6hR+pRqX4TCWqDWewCjNIOuQ==", "cpu": [ "x64" ], @@ -82,9 +82,9 @@ ] }, "node_modules/@github/copilot-language-server-linux-arm64": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.451.0.tgz", - "integrity": "sha512-eNemo1Nj9ZPpE+FhDqQe1iRHQdZZCgRTmXZ/hrfc7slqDyDrMuMII18l7lLHspN2Po8hNZdJjrMvnk0J9mebSw==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-arm64/-/copilot-language-server-linux-arm64-1.457.0.tgz", + "integrity": "sha512-y/T5jZL3UncYCkTD6pb3xqcAMfNMyOJc7fiMAMs91v33nkFx2qdoJoEt1DapVVMflX+rHUIrnIzunqjt3SpoKg==", "cpu": [ "arm64" ], @@ -95,9 +95,9 @@ ] }, "node_modules/@github/copilot-language-server-linux-x64": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.451.0.tgz", - "integrity": "sha512-cnTYzJElUb3xw4Y8gylIuY3rWwTeNWPbI848yf8sZVxDo+P7U8Sfyfo0ZIxbwF2r48EohQZ0PA9Uu3Q0pX9dEA==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-linux-x64/-/copilot-language-server-linux-x64-1.457.0.tgz", + "integrity": "sha512-dBmt7qETR5Xf2IzdVBtBw38VHQsOq4cf8Nxv9VdzjkV+qjSoS1Tsf7lYKR3O1uz6MKhJAS0spcy4c7soWOztRA==", "cpu": [ "x64" ], @@ -108,9 +108,9 @@ ] }, "node_modules/@github/copilot-language-server-win32-arm64": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-arm64/-/copilot-language-server-win32-arm64-1.451.0.tgz", - "integrity": "sha512-9Wx2XRZJm+8Fy2Ho2kuupBQpXyj9pSJJXO+Xi2oFFBSdS9pAEpqx+62CMTqLLjlmDkFj9QW0rI5FNDynxSPBCQ==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-arm64/-/copilot-language-server-win32-arm64-1.457.0.tgz", + "integrity": "sha512-ojEYwoa2Gq3DPkrfHq0lKt3dV8VZ0RcI2pkTCndSmtDP93bDr3Y7f0pBj4qh41IQG8iZwjmRAydIKZJ0rIldTQ==", "cpu": [ "arm64" ], @@ -121,9 +121,9 @@ ] }, "node_modules/@github/copilot-language-server-win32-x64": { - "version": "1.451.0", - "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.451.0.tgz", - "integrity": "sha512-L9uqgeQNWGr9Vrpj8fYwazAJYn/TwqrhZ2r/euXi7wpg8fPTHh9JAmdBLI39Gr34kyclL1fxjzvNzm0UtRC0XA==", + "version": "1.457.0", + "resolved": "https://registry.npmjs.org/@github/copilot-language-server-win32-x64/-/copilot-language-server-win32-x64-1.457.0.tgz", + "integrity": "sha512-VVdcoCuzoWeCfvcgPQwBuP6ZTkcJm13zxhoxVmxWH3TaCrGaTY2S9xKX5ZoATcLvMyE6b3phKU3px3lGaZr2Aw==", "cpu": [ "x64" ], diff --git a/Server/package.json b/Server/package.json index 3599040a..a669cca8 100644 --- a/Server/package.json +++ b/Server/package.json @@ -7,9 +7,9 @@ "build": "webpack" }, "dependencies": { - "@github/copilot-language-server": "1.451.0", - "@github/copilot-language-server-darwin-arm64": "1.451.0", - "@github/copilot-language-server-darwin-x64": "1.451.0", + "@github/copilot-language-server": "1.457.0", + "@github/copilot-language-server-darwin-arm64": "1.457.0", + "@github/copilot-language-server-darwin-x64": "1.457.0", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.5.0", "monaco-editor": "0.52.2"