Skip to main content
Glama
ClaudeDesktop.swift10.8 kB
import AppKit import Foundation import MCP import OSLog private let log = Logger.integration("claude-desktop") private let configPath = "/Users/\(NSUserName())/Library/Application Support/Claude/claude_desktop_config.json" private let configBookmarkKey = "me.mattt.iMCP.claudeConfigBookmark" private let jsonEncoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes] return encoder }() private let jsonDecoder = JSONDecoder() enum ClaudeDesktop { struct Config: Codable { struct MCPServer: Codable { var command: String var args: [String]? var env: [String: String]? } var mcpServers: [String: MCPServer] } enum Error: LocalizedError { case noLocationSelected var errorDescription: String? { switch self { case .noLocationSelected: return "No location selected to save config" } } } static func showConfigurationPanel() { do { log.debug("Loading existing Claude Desktop configuration") let (config, imcpServer) = try loadConfig() let fileExists = FileManager.default.fileExists(atPath: configPath) let alert = NSAlert() alert.messageText = "Set Up iMCP Server" alert.informativeText = """ This will \(fileExists ? "update" : "create") the iMCP server settings in Claude Desktop. Location: \(configPath) Your existing server configurations won't be affected. """ alert.addButton(withTitle: "Set Up") alert.addButton(withTitle: "Cancel") NSApp.activate(ignoringOtherApps: true) let alertResponse = alert.runModal() if alertResponse == .alertFirstButtonReturn { log.debug("User clicked Save, updating configuration") try updateConfig(config, upserting: imcpServer) log.notice("Configuration updated successfully") } else { log.debug("User cancelled configuration update") } } catch { log.error("Error configuring Claude Desktop: \(error.localizedDescription)") let alert = NSAlert() alert.messageText = "Configuration Error" alert.informativeText = error.localizedDescription alert.alertStyle = .critical alert.runModal() } } } private func getSecurityScopedConfigURL() throws -> URL? { log.debug("Attempting to get security-scoped config URL") guard let bookmarkData = UserDefaults.standard.data(forKey: configBookmarkKey) else { log.debug("No bookmark data found in UserDefaults") return nil } var isStale = false let url = try URL( resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) if isStale { log.debug("Bookmark data is stale but URL was resolved: \(url.path). Attempting to use it.") // We will still return the URL and let the caller try to use it and refresh the bookmark. } log.debug("Successfully retrieved security-scoped URL: \(url.path)") return url } private func saveSecurityScopedAccess(for url: URL) throws { log.debug("Creating security-scoped bookmark for URL: \(url.path)") let bookmarkData = try url.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) UserDefaults.standard.set(bookmarkData, forKey: configBookmarkKey) log.debug("Successfully saved security-scoped bookmark") } private func loadConfig() throws -> ([String: Value], ClaudeDesktop.Config.MCPServer) { log.debug("Creating default iMCP server configuration") let imcpServer = ClaudeDesktop.Config.MCPServer( command: Bundle.main.bundleURL .appendingPathComponent("Contents/MacOS/imcp-server") .path) var loadedConfiguration: [String: Value]? // 1. Try to load using security-scoped URL if let secureURL = try? getSecurityScopedConfigURL() { log.debug("Attempting to load from security-scoped URL: \(secureURL.path)") if secureURL.startAccessingSecurityScopedResource() { defer { secureURL.stopAccessingSecurityScopedResource() } if FileManager.default.fileExists(atPath: secureURL.path) { do { log.debug("Loading existing configuration from: \(secureURL.path)") let data = try Data(contentsOf: secureURL) loadedConfiguration = try jsonDecoder.decode([String: Value].self, from: data) log.debug( "Successfully loaded from security-scoped URL. Attempting to refresh bookmark." ) try saveSecurityScopedAccess(for: secureURL) // Refresh bookmark } catch { log.error( "Failed to load or decode from security-scoped URL \(secureURL.path): \(error.localizedDescription)" ) } } else { log.debug( "Security-scoped URL \(secureURL.path) does not point to an existing file.") } } else { log.debug( "Failed to start accessing security-scoped resource for URL: \(secureURL.path)") } } else { log.debug("No security-scoped URL obtained or an error occurred retrieving it.") } // 2. If config is still nil (not loaded via security scope), try to load from default direct path if loadedConfiguration == nil { let defaultURL = URL(fileURLWithPath: configPath) log.debug("Attempting to load from default direct path: \(defaultURL.path)") if FileManager.default.fileExists(atPath: defaultURL.path) { do { let data = try Data(contentsOf: defaultURL) loadedConfiguration = try jsonDecoder.decode([String: Value].self, from: data) log.debug( "Successfully loaded from default path. Attempting to save security bookmark for it." ) try saveSecurityScopedAccess(for: defaultURL) // Establish bookmark if loaded directly } catch { log.error( "Failed to load or decode from default path \(defaultURL.path): \(error.localizedDescription)" ) } } else { log.debug("Default config file \(defaultURL.path) does not exist.") } } // 3. Use loaded configuration or fall back to default if still nil let finalConfig = loadedConfiguration ?? { log.notice( "No existing config found or accessible after all attempts. Creating a new default configuration." ) return ["mcpServers": .object([:])] }() return (finalConfig, imcpServer) } private func updateConfig( _ config: [String: Value], upserting imcpServer: ClaudeDesktop.Config.MCPServer ) throws { // Update the iMCP server entry var updatedConfig = config let imcpServerValue = try Value(imcpServer) if var mcpServers = config["mcpServers"]?.objectValue { mcpServers["iMCP"] = imcpServerValue updatedConfig["mcpServers"] = .object(mcpServers) } else { updatedConfig["mcpServers"] = .object(["iMCP": imcpServerValue]) } // First try with the security-scoped URL if available if let secureURL = try? getSecurityScopedConfigURL() { if secureURL.startAccessingSecurityScopedResource() { defer { secureURL.stopAccessingSecurityScopedResource() } do { try writeConfig(updatedConfig, to: secureURL) return } catch { log.error("Failed to write to security-scoped URL: \(error.localizedDescription)") // Continue to fallback options } } else { log.error("Failed to access security-scoped resource") } } // Then try to use the default path directly if it exists and is writable let defaultURL = URL(fileURLWithPath: configPath) if FileManager.default.fileExists(atPath: configPath) { do { // Test if we can write to this file if FileManager.default.isWritableFile(atPath: configPath) { try writeConfig(updatedConfig, to: defaultURL) // Since we succeeded with direct path, create a bookmark for future use try? saveSecurityScopedAccess(for: defaultURL) return } } catch { log.error("Failed to write to default config path: \(error.localizedDescription)") // Continue to show save panel } } // Finally, show save panel as a last resort log.debug("Showing save panel for new configuration location") let savePanel = NSSavePanel() savePanel.message = "Choose where to save the iMCP server settings." savePanel.prompt = "Set Up" savePanel.allowedContentTypes = [.json] savePanel.directoryURL = URL(fileURLWithPath: configPath).deletingLastPathComponent() savePanel.nameFieldStringValue = "claude_desktop_config.json" savePanel.canCreateDirectories = true savePanel.showsHiddenFiles = false guard savePanel.runModal() == .OK, let selectedURL = savePanel.url else { log.error("No location selected to save configuration") throw ClaudeDesktop.Error.noLocationSelected } // Create the file first log.debug("Creating configuration at selected URL: \(selectedURL.path)") do { try writeConfig(updatedConfig, to: selectedURL) // Then create the security-scoped bookmark log.debug("Creating security-scoped access for selected URL") try saveSecurityScopedAccess(for: selectedURL) } catch { log.error("Failed to write config to selected URL: \(error)") throw error } } private func writeConfig(_ config: [String: Value], to url: URL) throws { log.debug("Creating directory if needed: \(url.deletingLastPathComponent().path)") try FileManager.default.createDirectory( at: url.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil ) log.debug("Encoding and writing configuration") let data = try jsonEncoder.encode(config) try data.write(to: url, options: .atomic) log.notice("Successfully saved config to \(url.path)") }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mattt/iMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server