├── .DS_Store ├── .gitattributes ├── Users └── toddbruss │ └── Documents │ └── GitHub │ └── xcf │ └── test_absolute_dir │ └── test.swift ├── xcf ├── Mcp │ ├── XcDoc │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Artboard 1.png │ │ │ │ ├── Artboard 2.png │ │ │ │ ├── Artboard 3.png │ │ │ │ ├── Artboard 4.png │ │ │ │ ├── Artboard 5.png │ │ │ │ ├── Artboard 6.png │ │ │ │ ├── Artboard 7.png │ │ │ │ ├── Artboard 8.png │ │ │ │ ├── Artboard 9.png │ │ │ │ ├── Artboard 10.png │ │ │ │ └── Contents.json │ │ │ └── AccentColor.colorset │ │ │ │ └── Contents.json │ │ ├── XcfMcpXcodeDocumentHandlers.swift │ │ └── XcfXcodeProjectManager.swift │ ├── Tools │ │ ├── XcfMcpHelpHandlers.swift │ │ ├── XcfMcpCodeAnalysisHandlers.swift │ │ ├── XcfMcpActionHandlers.swift │ │ ├── XcfMcpCodeSnippetHandlers.swift │ │ ├── XcfMcpToolCallHandlers.swift │ │ ├── XcfMcpDiffHandlers.swift │ │ └── XcfMcpFileSystemHandlers.swift │ ├── File │ │ ├── XcfFileFinder.swift │ │ ├── XcfFuzzyLogicService.swift │ │ └── XcfSecurityManager.swift │ ├── ServerConfig │ │ ├── XcfMcpServer.swift │ │ ├── XcfMcpHandlers.swift │ │ ├── XcfMcpConstants.swift │ │ └── XcfHelpText.swift │ ├── Resources │ │ ├── XcfMcpResourceHandlers.swift │ │ └── XcfMcpResources.swift │ ├── Script │ │ ├── XcfOsaScript.swift │ │ ├── XcfCaptureSnippet.swift │ │ ├── XcfSwiftDiff.swift │ │ └── XcfSwiftScript.swift │ └── Prompts │ │ ├── XcfMcpPromptHandlers.swift │ │ └── XcfMcpPrompts.swift ├── xcf.entitlements ├── main.swift └── ORGANIZATION_SUMMARY.md ├── xcf.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── toddbruss.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── toddbruss.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── xcshareddata │ └── xcschemes │ └── xcf.xcscheme ├── mcp.json ├── .cursor ├── mcp.json └── rules │ ├── xcf-quickref.mdc │ ├── xcf-ai-guide.mdc │ └── xcf-user-guide.mdc ├── .gitignore ├── LICENSE ├── xcf-quickref.md ├── xcf-ai-guide.md └── xcf-user-guide.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /Users/toddbruss/Documents/GitHub/xcf/test_absolute_dir/test.swift: -------------------------------------------------------------------------------- 1 | Testing absolute path: \(path) -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 1.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 2.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 3.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 4.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 5.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 6.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 7.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 8.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 9.png -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Artboard 10.png -------------------------------------------------------------------------------- /xcf.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /xcf/xcf.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeFreezeAI/xcf/HEAD/xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /xcf.xcodeproj/xcuserdata/toddbruss.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "xcf": { 4 | "type": "stdio", 5 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server", 6 | "env": { 7 | "XCODE_PROJECT_FOLDER_optional": "/path/to/project/", 8 | "XCODE_PROJECT_optional": "/path/to/project/project.xcodeproj" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.cursor/mcp.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "xcf": { 4 | "type": "stdio", 5 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server", 6 | "env": { 7 | "XCODE_PROJECT_FOLDER_optional": "/path/to/project/", 8 | "XCODE_PROJECT_optional": "/path/to/project/project.xcodeproj" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate 2 | xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate 3 | .DS_Store 4 | *.xcuserstate 5 | .DS_Store 6 | xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate 7 | .DS_Store 8 | xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate 9 | .DS_Store 10 | xcf.xcodeproj/project.xcworkspace/xcuserdata/toddbruss.xcuserdatad/UserInterfaceState.xcuserstate 11 | *.xcuserstate 12 | .DS_Store 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /xcf.xcodeproj/xcuserdata/toddbruss.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | xcf.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | EA8D401B2DD0533700464A89 16 | 17 | primary 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpHelpHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - Help & Documentation Tool Handlers 5 | 6 | class XcfMcpHelpHandlers { 7 | 8 | /// Handles a call to the quick help tool (xcf actions only) 9 | static func handleQuickHelpToolCall(_ params: CallTool.Parameters) -> CallTool.Result { 10 | return CallTool.Result(content: [.text(HelpText.basic)]) 11 | } 12 | 13 | /// Handles a call to the regular help tool 14 | static func handleHelpToolCall(_ params: CallTool.Parameters) -> CallTool.Result { 15 | return CallTool.Result(content: [.text(HelpText.detailed)]) 16 | } 17 | 18 | /// Handles a call to the tools reference tool 19 | static func handleToolsReferenceToolCall(_ params: CallTool.Parameters) -> CallTool.Result { 20 | return CallTool.Result(content: [.text(HelpText.toolsReference)]) 21 | } 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 SuperBox64 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Artboard 10.png", 5 | "idiom" : "mac", 6 | "scale" : "1x", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "filename" : "Artboard 9.png", 11 | "idiom" : "mac", 12 | "scale" : "2x", 13 | "size" : "16x16" 14 | }, 15 | { 16 | "filename" : "Artboard 8.png", 17 | "idiom" : "mac", 18 | "scale" : "1x", 19 | "size" : "32x32" 20 | }, 21 | { 22 | "filename" : "Artboard 7.png", 23 | "idiom" : "mac", 24 | "scale" : "2x", 25 | "size" : "32x32" 26 | }, 27 | { 28 | "filename" : "Artboard 6.png", 29 | "idiom" : "mac", 30 | "scale" : "1x", 31 | "size" : "128x128" 32 | }, 33 | { 34 | "filename" : "Artboard 5.png", 35 | "idiom" : "mac", 36 | "scale" : "2x", 37 | "size" : "128x128" 38 | }, 39 | { 40 | "filename" : "Artboard 4.png", 41 | "idiom" : "mac", 42 | "scale" : "1x", 43 | "size" : "256x256" 44 | }, 45 | { 46 | "filename" : "Artboard 3.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "Artboard 2.png", 53 | "idiom" : "mac", 54 | "scale" : "1x", 55 | "size" : "512x512" 56 | }, 57 | { 58 | "filename" : "Artboard 1.png", 59 | "idiom" : "mac", 60 | "scale" : "2x", 61 | "size" : "512x512" 62 | } 63 | ], 64 | "info" : { 65 | "author" : "xcode", 66 | "version" : 1 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /xcf/Mcp/File/XcfFileFinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfFileFinder.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/9/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A utility class to find files in the workspace using multiple strategies. 11 | struct FileFinder { 12 | /// Attempts to intelligently resolve a file path using multiple strategies 13 | /// - Parameter originalPath: The original path provided 14 | /// - Returns: A tuple containing (resolvedPath, warningMessage) where resolvedPath is the resolved path or original if no strategies succeed, and warningMessage contains any warnings about duplicate files 15 | static func resolveFilePath(_ originalPath: String) -> (String, String) { 16 | return FuzzyLogicService.resolveFilePath(originalPath) 17 | } 18 | 19 | /// Determines the language of a file based on its extension 20 | /// - Parameter filePath: The path to the file 21 | /// - Returns: A string indicating the language of the file 22 | public static func determineLanguage(from filePath: String) -> String { 23 | let fileExtension = (filePath as NSString).pathExtension.lowercased() 24 | 25 | switch fileExtension { 26 | case "swift": 27 | return "swift" 28 | case "m", "h": 29 | return "objc" 30 | case "c", "cpp", "cc": 31 | return "c" 32 | case "js": 33 | return "javascript" 34 | case "py": 35 | return "python" 36 | case "rb": 37 | return "ruby" 38 | case "java": 39 | return "java" 40 | case "html", "htm": 41 | return "html" 42 | case "css": 43 | return "css" 44 | case "json": 45 | return "json" 46 | case "xml": 47 | return "xml" 48 | default: 49 | return "text" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /xcf/Mcp/ServerConfig/XcfMcpServer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfMcpServer.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/7/25. 6 | // 7 | 8 | import Foundation 9 | import MCP 10 | 11 | /// McpServer handles all MCP protocol interactions for the xcf tool. 12 | /// It defines tools, resources, and prompts, and configures the MCP server with appropriate handlers. 13 | class XcfMcpServer { 14 | static var XcfScript = XcfSwiftScript.shared 15 | 16 | // MARK: - Core Server Setup 17 | 18 | /// Configures and starts the MCP server 19 | /// - Returns: The configured MCP server 20 | /// - Throws: Errors from server initialization or start 21 | static func configureMcpServer() async throws -> Server { 22 | let projectManager = XcfXcodeProjectManager.shared 23 | 24 | // Initialize the project manager 25 | projectManager.initialize() 26 | 27 | // If no project was set from environment, try to select one 28 | if projectManager.currentProject == nil { 29 | projectManager.currentProject = await XcfMcpActionHandler.selectProject() 30 | } 31 | 32 | // Set up the server with enhanced capabilities 33 | let server = Server( 34 | name: McpConfig.serverName, 35 | version: McpConfig.serverVersion, 36 | capabilities: .init( 37 | prompts: .init(listChanged: true), 38 | resources: .init(subscribe: true, listChanged: true), 39 | tools: .init(listChanged: true) 40 | ) 41 | ) 42 | 43 | // Configure the transport 44 | let transport = StdioTransport() 45 | 46 | do { 47 | // Start the server with the configured transport 48 | try await server.start(transport: transport) 49 | 50 | // Register all handlers using the new handler classes 51 | await XcfMcpHandlers.registerAllHandlers(server: server) 52 | 53 | return server 54 | } catch { 55 | throw error 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /xcf/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/3/25. 6 | // 7 | 8 | import Cocoa 9 | import MCP 10 | import MultiLineDiff 11 | 12 | // Process command line arguments 13 | func handleArguments() { 14 | // Check command-line arguments for "server" flag 15 | if CommandLine.arguments.count > 1 && CommandLine.arguments[1] == "server" { 16 | // Print welcome message 17 | 18 | // Main async task 19 | Task.detached { 20 | do { 21 | // Configure and start the MCP server 22 | let server = try await XcfMcpServer.configureMcpServer() 23 | 24 | // Wait until the server completes 25 | await server.waitUntilCompleted() 26 | } catch { 27 | print(String(format: McpConfig.errorStartingServer, "\(error)")) 28 | } 29 | } 30 | 31 | // Initialize the application 32 | NSApplication.shared.setActivationPolicy(.prohibited) 33 | RunLoop.current.run() 34 | 35 | } else { 36 | NSApplication.shared.setActivationPolicy(.accessory) 37 | 38 | ModalDisplay().showServerRequiredAlert() 39 | NSApplication.shared.terminate(nil) 40 | 41 | } 42 | } 43 | 44 | 45 | class ModalDisplay { 46 | 47 | func showServerRequiredAlert() { 48 | let alert = NSAlert() 49 | alert.messageText = "Use 'server' argument in your mcp.json" 50 | alert.informativeText = "/Applications/xcf.app/Contents/MacOS/xcf server" 51 | alert.alertStyle = .warning 52 | let quitButton = alert.addButton(withTitle: "Press to Quit this XCF Xcode MCP Server") 53 | quitButton.hasDestructiveAction = false 54 | quitButton.keyEquivalent = "\r" // Return key 55 | alert.runModal() 56 | 57 | 58 | } 59 | } 60 | 61 | // Set up signal handling for clean shutdown 62 | signal(SIGINT) { _ in 63 | print("\nShutting down...") 64 | exit(0) 65 | } 66 | 67 | signal(SIGTERM) { _ in 68 | print("\nShutting down...") 69 | exit(0) 70 | } 71 | 72 | handleArguments() 73 | -------------------------------------------------------------------------------- /xcf.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "b58ab3d2524826d1ee6381aab41445f36db72aa6c60122ae51658543ea6e9fee", 3 | "pins" : [ 4 | { 5 | "identity" : "eventsource", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/mattt/eventsource.git", 8 | "state" : { 9 | "revision" : "07957602bb99a5355c810187e66e6ce378a1057d", 10 | "version" : "1.1.1" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-log", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/apple/swift-log.git", 17 | "state" : { 18 | "revision" : "ce592ae52f982c847a4efc0dd881cc9eb32d29f2", 19 | "version" : "1.6.4" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-multi-line-diff", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/codefreezeai/swift-multi-line-diff.git", 26 | "state" : { 27 | "revision" : "696849afadd73e2fbecb64a2f2e1272d17ea298c", 28 | "version" : "2.0.2" 29 | } 30 | }, 31 | { 32 | "identity" : "swift-sdk", 33 | "kind" : "remoteSourceControl", 34 | "location" : "https://github.com/modelcontextprotocol/swift-sdk.git", 35 | "state" : { 36 | "revision" : "f1b50e6de22b5206068bb09851d585f560d893c4", 37 | "version" : "0.10.1" 38 | } 39 | }, 40 | { 41 | "identity" : "swift-syntax", 42 | "kind" : "remoteSourceControl", 43 | "location" : "https://github.com/apple/swift-syntax.git", 44 | "state" : { 45 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", 46 | "version" : "601.0.1" 47 | } 48 | }, 49 | { 50 | "identity" : "swift-system", 51 | "kind" : "remoteSourceControl", 52 | "location" : "https://github.com/apple/swift-system.git", 53 | "state" : { 54 | "revision" : "890830fff1a577dc83134890c7984020c5f6b43b", 55 | "version" : "1.6.2" 56 | } 57 | }, 58 | { 59 | "identity" : "xcf-swift", 60 | "kind" : "remoteSourceControl", 61 | "location" : "https://github.com/codefreezeai/xcf-swift", 62 | "state" : { 63 | "revision" : "2bc70856401230b093956dee35ac9bec174fec62", 64 | "version" : "1.0.0" 65 | } 66 | } 67 | ], 68 | "version" : 3 69 | } 70 | -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpCodeAnalysisHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - Code Analysis Tool Handlers 5 | 6 | class XcfMcpCodeAnalysisHandlers { 7 | 8 | /// Handles a call to the snippet tool 9 | static func handleSnippetToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 10 | guard let arguments = params.arguments else { 11 | return CallTool.Result(content: [.text(McpConfig.missingFilePathError)]) 12 | } 13 | 14 | // Try to get filePath from arguments in two ways: 15 | // 1. As a named parameter (filePath=...) 16 | // 2. As a direct argument (first argument after command) 17 | let filePath: String 18 | if let namedPath = arguments[McpConfig.filePathParamName]?.stringValue { 19 | filePath = namedPath 20 | } else if let firstArg = arguments.first?.value.stringValue { 21 | filePath = firstArg 22 | } else { 23 | return CallTool.Result(content: [.text(McpConfig.missingFilePathError)]) 24 | } 25 | 26 | let entireFile = arguments[McpConfig.entireFileParamName]?.boolValue ?? false 27 | let startLine = arguments[McpConfig.startLineParamName]?.intValue 28 | let endLine = arguments[McpConfig.endLineParamName]?.intValue 29 | 30 | return XcfMcpCodeSnippetHandlers.handleCodeSnippet( 31 | filePath: filePath, 32 | entireFile: entireFile, 33 | startLine: startLine, 34 | endLine: endLine 35 | ) 36 | } 37 | 38 | /// Handles a call to the analyzer tool 39 | static func handleAnalyzerToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 40 | guard let arguments = params.arguments else { 41 | return CallTool.Result(content: [.text(McpConfig.missingFilePathError)]) 42 | } 43 | 44 | // Try to get filePath from arguments in two ways: 45 | // 1. As a named parameter (filePath=...) 46 | // 2. As a direct argument (first argument after command) 47 | let filePath: String 48 | if let namedPath = arguments[McpConfig.filePathParamName]?.stringValue { 49 | filePath = namedPath 50 | } else if let firstArg = arguments.first?.value.stringValue { 51 | filePath = firstArg 52 | } else { 53 | return CallTool.Result(content: [.text(McpConfig.missingFilePathError)]) 54 | } 55 | 56 | let entireFile = arguments[McpConfig.entireFileParamName]?.boolValue ?? false 57 | let startLine = arguments[McpConfig.startLineParamName]?.intValue 58 | let endLine = arguments[McpConfig.endLineParamName]?.intValue 59 | 60 | return XcfMcpCodeSnippetHandlers.handleAnalyzerCodeSnippet( 61 | filePath: filePath, 62 | entireFile: entireFile, 63 | startLine: startLine, 64 | endLine: endLine 65 | ) 66 | } 67 | } -------------------------------------------------------------------------------- /xcf/Mcp/Resources/XcfMcpResourceHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - MCP Resource Handlers 5 | 6 | class XcfMcpResourceHandlers { 7 | 8 | /// Handles a resource request 9 | static func handleResourceRequest(_ params: ReadResource.Parameters) async throws -> ReadResource.Result { 10 | switch params.uri { 11 | case McpConfig.xcodeProjResourceURI: 12 | return try await handleXcodeProjectsResource() 13 | 14 | case McpConfig.fileContentsResourceURI: 15 | return try handleFileContentsResource(uri: params.uri) 16 | 17 | case McpConfig.buildResultsResourceURI: 18 | return try await handleBuildResultsResource() 19 | 20 | default: 21 | throw MCPError.invalidParams(String(format: McpConfig.unknownResourceUriError, params.uri)) 22 | } 23 | } 24 | 25 | /// Handles a request for the Xcode projects resource 26 | static func handleXcodeProjectsResource() async throws -> ReadResource.Result { 27 | let projects = await XcfMcpActionHandler.getSortedXcodeProjects() 28 | let content = Resource.Content.text( 29 | projects.joined(separator: McpConfig.newLineSeparator), 30 | uri: McpConfig.xcodeProjResourceURI 31 | ) 32 | return ReadResource.Result(contents: [content]) 33 | } 34 | 35 | /// Handles a request for the build results resource 36 | static func handleBuildResultsResource() async throws -> ReadResource.Result { 37 | guard let currentProject = await XcfMcpActionHandler.getCurrentProject() else { 38 | throw MCPError.invalidParams(ErrorMessages.noProjectSelected) 39 | } 40 | 41 | let buildResults = XcfMcpServer.XcfScript.buildCurrentWorkspace(projectPath: currentProject, run: false) 42 | let content = Resource.Content.text( 43 | buildResults, 44 | uri: McpConfig.buildResultsResourceURI 45 | ) 46 | return ReadResource.Result(contents: [content]) 47 | } 48 | 49 | /// Handles a request for the file contents resource 50 | static func handleFileContentsResource(uri: String) throws -> ReadResource.Result { 51 | guard let filePath = XcfMcpHandlers.extractFilePathFromUri(uri) else { 52 | throw MCPError.invalidParams(McpConfig.missingFilePathParamError) 53 | } 54 | 55 | do { 56 | let (fileContents, warning) = try XcfFileManager.readFile(at: filePath) 57 | let content = Resource.Content.text( 58 | warning.isEmpty ? fileContents : warning + "\n\n" + fileContents, 59 | uri: "\(McpConfig.fileContentsResourceURI)/\(filePath)", 60 | mimeType: McpConfig.plainTextMimeType 61 | ) 62 | return ReadResource.Result(contents: [content]) 63 | } catch let error as NSError { 64 | throw MCPError.invalidParams(error.localizedDescription) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /xcf.xcodeproj/xcshareddata/xcschemes/xcf.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /xcf/Mcp/Script/XcfOsaScript.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfOsascript.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/4/25. 6 | // 7 | 8 | import Foundation 9 | 10 | //MARK: Some Systems only approve over OSAScript - this is a workaround 11 | //TODO: Monitor and see if we can switch the approval to ScriptingBridge 12 | func grantAutomation() -> String { 13 | """ 14 | tell application "Xcode" 15 | set xcDoc to first document 16 | 17 | tell xcDoc 18 | set buildResult to build 19 | 20 | repeat 21 | if completed of buildResult is true then 22 | exit repeat 23 | end if 24 | delay 0.5 25 | end repeat 26 | 27 | return "Xcode Automation permission has been granted" 28 | end tell 29 | end tell 30 | """ 31 | } 32 | 33 | func executeWithOsascript(script: String, timeout: TimeInterval = 30.0) -> String { 34 | let process = Process() 35 | process.executableURL = URL(fileURLWithPath: Paths.osascriptPath) 36 | 37 | // Create a pipe to capture the output 38 | let outputPipe = Pipe() 39 | let errorPipe = Pipe() // Add error pipe to capture stderr 40 | 41 | process.standardOutput = outputPipe 42 | process.standardError = errorPipe 43 | process.arguments = ["-e", script] 44 | 45 | do { 46 | try process.run() 47 | 48 | // Create a timeout 49 | let waitSemaphore = DispatchSemaphore(value: 0) 50 | let timeoutTask = DispatchWorkItem { 51 | if process.isRunning { 52 | process.terminate() 53 | } 54 | } 55 | 56 | // Schedule timeout 57 | DispatchQueue.global().asyncAfter(deadline: .now() + timeout, execute: timeoutTask) 58 | 59 | // Wait in background 60 | DispatchQueue.global().async { 61 | process.waitUntilExit() 62 | timeoutTask.cancel() // Cancel timeout if process completes normally 63 | waitSemaphore.signal() 64 | } 65 | 66 | // Wait for completion or timeout 67 | let waitResult = waitSemaphore.wait(timeout: .now() + timeout + 1.0) 68 | if waitResult == .timedOut { 69 | return "Error: Process timed out after \(timeout) seconds" 70 | } 71 | 72 | // Check process exit status 73 | if process.terminationStatus != 0 { 74 | let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() 75 | if let errorString = String(data: errorData, encoding: .utf8), !errorString.isEmpty { 76 | return String(format: ErrorMessages.failedToExecuteOsascript, errorString) 77 | } 78 | } 79 | 80 | // Read the output data 81 | let data = outputPipe.fileHandleForReading.readDataToEndOfFile() 82 | 83 | // Convert the data to a string 84 | if let result = String(data: data, encoding: .utf8) { 85 | return result.trimmingCharacters(in: .whitespacesAndNewlines) 86 | } else { 87 | return ErrorMessages.failedToConvertOutput 88 | } 89 | } catch { 90 | return String(format: ErrorMessages.failedToExecuteOsascript, "\(error)") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /xcf/Mcp/Prompts/XcfMcpPromptHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - MCP Prompt Handlers 5 | 6 | class XcfMcpPromptHandlers { 7 | 8 | /// Handles a prompt request 9 | static func handlePromptRequest(_ params: GetPrompt.Parameters) throws -> GetPrompt.Result { 10 | switch params.name { 11 | case McpConfig.buildPromptName: 12 | return handleBuildPrompt(params) 13 | 14 | case McpConfig.runPromptName: 15 | return handleRunPrompt(params) 16 | 17 | case McpConfig.analyzeCodePromptName: 18 | return try handleAnalyzeCodePrompt(params) 19 | 20 | default: 21 | throw MCPError.invalidParams(String(format: McpConfig.unknownPromptNameError, params.name)) 22 | } 23 | } 24 | 25 | /// Handles a request for the build project prompt 26 | static func handleBuildPrompt(_ params: GetPrompt.Parameters) -> GetPrompt.Result { 27 | // Get project path from arguments or use placeholder 28 | let projectPath = params.arguments?[McpConfig.projectPathArgName]?.stringValue ?? McpConfig.projectPathPlaceholder 29 | 30 | let messages: [Prompt.Message] = [ 31 | .user(.text(text: String(format: McpConfig.buildProjectTemplate, projectPath))) 32 | ] 33 | 34 | return GetPrompt.Result(description: McpConfig.buildProjectResultDesc, messages: messages) 35 | } 36 | 37 | /// Handles a request for the run project prompt 38 | static func handleRunPrompt(_ params: GetPrompt.Parameters) -> GetPrompt.Result { 39 | // Get project path from arguments or use placeholder 40 | let projectPath = params.arguments?[McpConfig.projectPathArgName]?.stringValue ?? McpConfig.projectPathPlaceholder 41 | 42 | let messages: [Prompt.Message] = [ 43 | .user(.text(text: String(format: McpConfig.runProjectTemplate, projectPath))) 44 | ] 45 | 46 | return GetPrompt.Result(description: McpConfig.runProjectResultDesc, messages: messages) 47 | } 48 | 49 | /// Handles a request for the analyze code prompt 50 | static func handleAnalyzeCodePrompt(_ params: GetPrompt.Parameters) throws -> GetPrompt.Result { 51 | // Get file path from arguments or use placeholder 52 | let filePath = params.arguments?[McpConfig.filePathArgName]?.stringValue ?? McpConfig.filePathPlaceholder 53 | let includeSnippet = params.arguments?[McpConfig.includeSnippetArgName]?.boolValue ?? false 54 | 55 | // Create the base message 56 | let baseMessage = Prompt.Message.user( 57 | .text(text: String(format: McpConfig.analyzeCodeTemplate, filePath)) 58 | ) 59 | 60 | var messages: [Prompt.Message] = [baseMessage] 61 | 62 | // Add code snippet if requested 63 | if includeSnippet { 64 | if let snippetMessage = try? XcfMcpCodeSnippetHandlers.createCodeSnippetMessage(filePath: filePath) { 65 | messages.append(snippetMessage) 66 | } 67 | // If snippet creation fails, we still return the base message 68 | } 69 | 70 | return GetPrompt.Result(description: McpConfig.analyzeCodeResultDesc, messages: messages) 71 | } 72 | } -------------------------------------------------------------------------------- /xcf/Mcp/Script/XcfCaptureSnippet.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfCaptureSnippets.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/6/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A utility struct that provides functionality for capturing code snippets from files. 11 | /// This struct offers methods to extract specific line ranges from source code files 12 | /// and determine the programming language based on file extensions. 13 | struct CaptureSnippet { 14 | /// Extracts a code snippet from a file at the specified line range. 15 | /// 16 | /// This function attempts to locate the file using FuzzyLogicService, 17 | /// then extracts either the specified line range or the entire file content. 18 | /// It also determines the programming language based on the file extension. 19 | /// 20 | /// - Parameters: 21 | /// - filePath: The path to the source code file 22 | /// - startLine: The first line to include in the snippet (1-indexed) 23 | /// - endLine: The last line to include in the snippet (1-indexed) 24 | /// - entireFile: If true, returns the entire file content regardless of line numbers 25 | /// - Returns: A tuple containing (snippet text with any warnings, detected language) 26 | static func getCodeSnippet(filePath: String, startLine: Int, endLine: Int, entireFile: Bool = false) -> (String, String) { 27 | var snippet = "" 28 | let language = FileFinder.determineLanguage(from: filePath) 29 | 30 | // Try to find the file using FuzzyLogicService 31 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 32 | 33 | // If we got a warning about duplicate files, prepend it to the snippet 34 | var warningMessage = "" 35 | if !warning.isEmpty { 36 | warningMessage = warning + Format.newLine + Format.newLine 37 | } 38 | 39 | do { 40 | let fileContents = try String(contentsOfFile: resolvedPath, encoding: .utf8) 41 | 42 | if entireFile { 43 | // If entireFile is true, return the whole file content regardless of line numbers 44 | return (warningMessage + fileContents, language) 45 | } 46 | 47 | let lines = fileContents.components(separatedBy: Format.newlinesCharSet()) 48 | 49 | // Validate line numbers 50 | if startLine < 1 || endLine > lines.count || endLine < startLine { 51 | return (warningMessage + ErrorMessages.invalidLineNumbers, language) 52 | } 53 | 54 | // Extract the snippet 55 | for i in startLine...min(endLine, lines.count) { 56 | if i > startLine { 57 | snippet += Format.newLine 58 | } 59 | if i-1 < lines.count { 60 | snippet += lines[i-1] 61 | } 62 | } 63 | } catch { 64 | return (String(format: ErrorMessages.errorReadingFile, error.localizedDescription), language) 65 | } 66 | 67 | return (warningMessage + snippet, language) 68 | } 69 | 70 | /// Determines the programming language based on the file extension 71 | /// 72 | /// This function examines the file extension and maps it to a language 73 | /// identifier string that can be used for syntax highlighting or other 74 | /// language-specific processing. 75 | /// 76 | /// - Parameter filePath: The path to the file 77 | /// - Returns: A string identifier for the detected programming language 78 | public static func determineLanguage(from filePath: String) -> String { 79 | return FileFinder.determineLanguage(from: filePath) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /xcf/Mcp/Resources/XcfMcpResources.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - MCP Resources 5 | 6 | class XcfMcpResources { 7 | 8 | static func getAllResources() -> [Resource] { 9 | return [ 10 | createXcodeProjResource(), 11 | createBuildResultsResource(), 12 | createCodeSnippetResource(), 13 | createCodeAnalysisResource(), 14 | createXcodeDocumentResource(), 15 | createHelpResource(), 16 | createPermissionResource(), 17 | createProjectManagementResource(), 18 | createEnvironmentResource(), 19 | createDirectoryResource() 20 | ] 21 | } 22 | 23 | // MARK: - Resource Creation Functions 24 | 25 | private static func createXcodeProjResource() -> Resource { 26 | Resource( 27 | name: McpConfig.xcodeProjResourceName, 28 | uri: McpConfig.xcodeProjResourceURI, 29 | description: McpConfig.xcodeProjResourceDesc 30 | ) 31 | } 32 | 33 | private static func createBuildResultsResource() -> Resource { 34 | Resource( 35 | name: McpConfig.buildResultsResourceName, 36 | uri: McpConfig.buildResultsResourceURI, 37 | description: McpConfig.buildResultsResourceDesc 38 | ) 39 | } 40 | 41 | private static func createCodeSnippetResource() -> Resource { 42 | Resource( 43 | name: "codeSnippet", 44 | uri: "\(AppConstants.appName)://resources/codeSnippet", 45 | description: "Extracted code snippets from files" 46 | ) 47 | } 48 | 49 | private static func createCodeAnalysisResource() -> Resource { 50 | Resource( 51 | name: "codeAnalysis", 52 | uri: "\(AppConstants.appName)://resources/codeAnalysis", 53 | description: "Results of code analysis" 54 | ) 55 | } 56 | 57 | private static func createXcodeDocumentResource() -> Resource { 58 | Resource( 59 | name: "xcodeDocument", 60 | uri: "\(AppConstants.appName)://resources/xcodeDocument", 61 | description: "Content and status of Xcode documents" 62 | ) 63 | } 64 | 65 | private static func createHelpResource() -> Resource { 66 | Resource( 67 | name: McpConfig.helpResourceName, 68 | uri: McpConfig.helpResourceURI, 69 | description: McpConfig.helpResourceDesc 70 | ) 71 | } 72 | 73 | private static func createPermissionResource() -> Resource { 74 | Resource( 75 | name: McpConfig.permissionResourceName, 76 | uri: McpConfig.permissionResourceURI, 77 | description: McpConfig.permissionResourceDesc 78 | ) 79 | } 80 | 81 | private static func createProjectManagementResource() -> Resource { 82 | Resource( 83 | name: McpConfig.projectManagementResourceName, 84 | uri: McpConfig.projectManagementResourceURI, 85 | description: McpConfig.projectManagementResourceDesc 86 | ) 87 | } 88 | 89 | private static func createEnvironmentResource() -> Resource { 90 | Resource( 91 | name: McpConfig.environmentResourceName, 92 | uri: McpConfig.environmentResourceURI, 93 | description: McpConfig.environmentResourceDesc 94 | ) 95 | } 96 | 97 | private static func createDirectoryResource() -> Resource { 98 | Resource( 99 | name: McpConfig.directoryResourceName, 100 | uri: McpConfig.directoryResourceURI, 101 | description: McpConfig.directoryResourceDesc 102 | ) 103 | } 104 | } -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpActionHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - Action-Specific Tool Handlers 5 | 6 | class XcfMcpActionHandlers { 7 | 8 | /// Handles a call to the show help tool 9 | static func handleShowHelpToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 10 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.getHelpText())]) 11 | } 12 | 13 | /// Handles a call to the grant permission tool 14 | static func handleGrantPermissionToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 15 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.grantPermission())]) 16 | } 17 | 18 | /// Handles a call to the run project tool 19 | static func handleRunProjectToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 20 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.runProject())]) 21 | } 22 | 23 | /// Handles a call to the build project tool 24 | static func handleBuildProjectToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 25 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.buildProject())]) 26 | } 27 | 28 | /// Handles a call to the show current project tool 29 | static func handleShowCurrentProjectToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 30 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.showCurrentProject())]) 31 | } 32 | 33 | /// Handles a call to the show environment variables tool 34 | static func handleShowEnvToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 35 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.showEnvironmentVariables())]) 36 | } 37 | 38 | /// Handles a call to the show current folder tool 39 | static func handleShowFolderToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 40 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.showCurrentFolder())]) 41 | } 42 | 43 | /// Handles a call to the list projects tool 44 | static func handleListProjectsToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 45 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.listProjects())]) 46 | } 47 | 48 | /// Handles a call to the select project tool 49 | static func handleSelectProjectToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 50 | if let projectNumber = params.arguments?[McpConfig.projectNumberParamName]?.intValue { 51 | let action = "open \(projectNumber)" 52 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.selectProject(action: action))]) 53 | } else { 54 | return CallTool.Result(content: [.text(ErrorMessages.invalidProjectSelection)]) 55 | } 56 | } 57 | 58 | /// Handles a call to the analyze Swift code tool 59 | static func handleAnalyzeSwiftCodeToolCall(_ params: CallTool.Parameters) async -> CallTool.Result { 60 | guard let filePath = params.arguments?[McpConfig.filePathParamName]?.stringValue else { 61 | return CallTool.Result(content: [.text(McpConfig.missingFilePathError)]) 62 | } 63 | 64 | var analyzeCommand = "analyze \(filePath)" 65 | 66 | // Add optional parameters if provided 67 | if let startLine = params.arguments?[McpConfig.startLineParamName]?.intValue, 68 | let endLine = params.arguments?[McpConfig.endLineParamName]?.intValue { 69 | analyzeCommand += " \(startLine) \(endLine)" 70 | } 71 | 72 | // Add check groups if provided 73 | if let checkGroups = params.arguments?[McpConfig.checkGroupsParamName]?.arrayValue, 74 | !checkGroups.isEmpty { 75 | for group in checkGroups { 76 | if let groupName = group.stringValue { 77 | analyzeCommand += " \(groupName)" 78 | } 79 | } 80 | } 81 | 82 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.handleAnalyzeAction(action: analyzeCommand))]) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /xcf/Mcp/ServerConfig/XcfMcpHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | import MultiLineDiff 4 | 5 | typealias StringIndex = String.Index 6 | 7 | // MARK: - MCP Handlers 8 | 9 | class XcfMcpHandlers { 10 | 11 | // MARK: - Handler Registration 12 | 13 | /// Registers all method handlers with the server 14 | static func registerAllHandlers(server: Server) async { 15 | await registerToolsHandlers(server: server) 16 | await registerResourcesHandlers(server: server) 17 | await registerPromptsHandlers(server: server) 18 | } 19 | 20 | /// Registers handlers for tool-related methods 21 | private static func registerToolsHandlers(server: Server) async { 22 | // Handle tool listing 23 | await server.withMethodHandler(ListTools.self) { _ in 24 | ListTools.Result(tools: XcfMcpTools.getAllTools()) 25 | } 26 | 27 | // Handle tool calls - delegate to specialized handler 28 | await server.withMethodHandler(CallTool.self) { params in 29 | try await XcfMcpToolCallHandlers.handleToolCall(params) 30 | } 31 | } 32 | 33 | /// Registers handlers for resource-related methods 34 | private static func registerResourcesHandlers(server: Server) async { 35 | // Handle resource listing 36 | await server.withMethodHandler(ListResources.self) { _ in 37 | ListResources.Result(resources: XcfMcpResources.getAllResources()) 38 | } 39 | 40 | // Handle resource retrieval - delegate to specialized handler 41 | await server.withMethodHandler(ReadResource.self) { params in 42 | try await XcfMcpResourceHandlers.handleResourceRequest(params) 43 | } 44 | } 45 | 46 | /// Registers handlers for prompt-related methods 47 | private static func registerPromptsHandlers(server: Server) async { 48 | // Handle prompt listing 49 | await server.withMethodHandler(ListPrompts.self) { _ in 50 | ListPrompts.Result(prompts: XcfMcpPrompts.getAllPrompts()) 51 | } 52 | 53 | // Handle prompt retrieval - delegate to specialized handler 54 | await server.withMethodHandler(GetPrompt.self) { params in 55 | try XcfMcpPromptHandlers.handlePromptRequest(params) 56 | } 57 | } 58 | 59 | // MARK: - Utility Methods 60 | 61 | /// Extracts a file path from a URI query string 62 | static func extractFilePathFromUri(_ uri: String) -> String? { 63 | let uriComponents = uri.components(separatedBy: "?") 64 | guard uriComponents.count > 1, 65 | let queryItems = URLComponents(string: "?" + uriComponents[1])?.queryItems, 66 | let filePathItem = queryItems.first(where: { $0.name == McpConfig.filePathQueryParam }), 67 | let filePath = filePathItem.value, 68 | !filePath.isEmpty else { 69 | return nil 70 | } 71 | return filePath 72 | } 73 | 74 | /// Generate Tool List 75 | static func getToolsList() -> String { 76 | var result = McpConfig.availableTools 77 | 78 | for tool in XcfMcpTools.getAllTools() { 79 | result += String(format: McpConfig.toolListFormat, tool.name, tool.description) 80 | } 81 | 82 | return result 83 | } 84 | 85 | static func getResourcesList() -> String { 86 | var result = McpConfig.availableResources 87 | 88 | for resource in XcfMcpResources.getAllResources() { 89 | result += String(format: McpConfig.resourceListFormat, 90 | resource.name, 91 | resource.uri, 92 | resource.description ?? "") 93 | } 94 | 95 | return result 96 | } 97 | 98 | static func getPromptsList() -> String { 99 | var result = McpConfig.availablePrompts 100 | 101 | for prompt in XcfMcpPrompts.getAllPrompts() { 102 | result += String(format: McpConfig.promptListFormat, 103 | prompt.name, 104 | prompt.description ?? "") 105 | } 106 | 107 | return result 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpCodeSnippetHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - Code Snippet Handlers 5 | 6 | class XcfMcpCodeSnippetHandlers { 7 | 8 | // MARK: - Code Snippet Handling 9 | 10 | /// Handles code snippet extraction 11 | static func handleCodeSnippet(filePath: String, entireFile: Bool, startLine: Int? = nil, endLine: Int? = nil) -> CallTool.Result { 12 | // Resolve the file path using multiple strategies 13 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 14 | 15 | // Validate file path - use the resolved path 16 | guard FileManager.default.fileExists(atPath: resolvedPath) else { 17 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorReadingFile, "File not found. Tried searching for \(filePath) in multiple locations."))]) 18 | } 19 | 20 | // Add warning message if there is one 21 | var warningText = "" 22 | if !warning.isEmpty { 23 | warningText = warning + Format.newLine + Format.newLine 24 | } 25 | 26 | if entireFile { 27 | return handleEntireFileSnippet(filePath: resolvedPath, warning: warningText) 28 | } else if let startLine = startLine, let endLine = endLine { 29 | return handleLineRangeSnippet(filePath: resolvedPath, startLine: startLine, endLine: endLine, warning: warningText) 30 | } else { 31 | return CallTool.Result(content: [.text(McpConfig.missingLineParamsError)]) 32 | } 33 | } 34 | 35 | /// Handles extracting an entire file as a code snippet 36 | static func handleEntireFileSnippet(filePath: String, warning: String = "") -> CallTool.Result { 37 | do { 38 | let fileContents = try String(contentsOfFile: filePath, encoding: .utf8) 39 | let language = FileFinder.determineLanguage(from: filePath) 40 | return CallTool.Result(content: [.text(warning + String(format: McpConfig.codeBlockFormat, language, fileContents))]) 41 | } catch { 42 | return CallTool.Result(content: [.text(warning + String(format: ErrorMessages.errorReadingFile, error.localizedDescription))]) 43 | } 44 | } 45 | 46 | /// Handles extracting a range of lines as a code snippet 47 | static func handleLineRangeSnippet(filePath: String, startLine: Int, endLine: Int, warning: String = "") -> CallTool.Result { 48 | let (snippet, language) = CaptureSnippet.getCodeSnippet( 49 | filePath: filePath, 50 | startLine: startLine, 51 | endLine: endLine 52 | ) 53 | 54 | return CallTool.Result(content: [.text(warning + String(format: McpConfig.codeBlockFormat, language, snippet))]) 55 | } 56 | 57 | /// Handles extracting a code snippet for the analyzer tool 58 | static func handleAnalyzerCodeSnippet(filePath: String, entireFile: Bool, startLine: Int? = nil, endLine: Int? = nil) -> CallTool.Result { 59 | // Use SwiftAnalyzer to analyze the code 60 | let (analysisResult, language) = SwiftAnalyzer.analyzeCode( 61 | filePath: filePath, 62 | entireFile: entireFile, 63 | startLine: startLine, 64 | endLine: endLine 65 | ) 66 | 67 | // Format the result based on the language 68 | if language == "markdown" { 69 | // Markdown output doesn't need code block formatting 70 | return CallTool.Result(content: [.text(analysisResult)]) 71 | } else { 72 | // Format as code block 73 | return CallTool.Result(content: [.text(String(format: McpConfig.codeBlockFormat, language, analysisResult))]) 74 | } 75 | } 76 | 77 | /// Creates a message containing a code snippet 78 | static func createCodeSnippetMessage(filePath: String) throws -> Prompt.Message { 79 | let fileContents = try String(contentsOfFile: filePath, encoding: .utf8) 80 | let language = FileFinder.determineLanguage(from: filePath) 81 | let resourceUri = "\(McpConfig.fileContentsResourceURI)/\(filePath)" 82 | 83 | return Prompt.Message.user( 84 | .resource( 85 | uri: resourceUri, 86 | mimeType: McpConfig.plainTextMimeType, 87 | text: String(format: McpConfig.codeBlockFormat, language, fileContents), 88 | blob: nil 89 | ) 90 | ) 91 | } 92 | } -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpToolCallHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | import MultiLineDiff 4 | 5 | //typealias StringIndex = String.Index 6 | 7 | // MARK: - MCP Tool Call Handlers 8 | 9 | class XcfMcpToolCallHandlers { 10 | 11 | // MARK: - Main Tool Call Handler 12 | 13 | /// Handles a tool call request 14 | static func handleToolCall(_ params: CallTool.Parameters) async throws -> CallTool.Result { 15 | switch params.name { 16 | case McpConfig.listToolsName: 17 | return CallTool.Result(content: [.text(XcfMcpHandlers.getToolsList())]) 18 | 19 | case McpConfig.xcfToolName: 20 | return try await handleXcfToolCall(params) 21 | 22 | case McpConfig.snippetToolName: 23 | return try XcfMcpCodeAnalysisHandlers.handleSnippetToolCall(params) 24 | 25 | case McpConfig.analyzerToolName: 26 | return try XcfMcpCodeAnalysisHandlers.handleAnalyzerToolCall(params) 27 | 28 | case McpConfig.quickHelpToolName: 29 | return XcfMcpHelpHandlers.handleQuickHelpToolCall(params) 30 | 31 | case McpConfig.helpToolName: 32 | return XcfMcpHelpHandlers.handleHelpToolCall(params) 33 | 34 | case McpConfig.useXcfToolName: 35 | return CallTool.Result(content: [.text(SuccessMessages.xcfActive)]) 36 | 37 | case McpConfig.readFileToolName: 38 | return try XcfMcpFileSystemHandlers.handleReadFileToolCall(params) 39 | 40 | case McpConfig.cdDirToolName: 41 | return try XcfMcpFileSystemHandlers.handleCdDirToolCall(params) 42 | 43 | case McpConfig.openDocToolName: 44 | return try XcfMcpXcodeDocumentHandlers.handleOpenDocToolCall(params) 45 | 46 | case McpConfig.createDocToolName: 47 | return try XcfMcpXcodeDocumentHandlers.handleCreateDocToolCall(params) 48 | 49 | case McpConfig.readDocToolName: 50 | return try XcfMcpXcodeDocumentHandlers.handleReadDocToolCall(params) 51 | 52 | case McpConfig.saveDocToolName: 53 | return try XcfMcpXcodeDocumentHandlers.handleSaveDocToolCall(params) 54 | 55 | case McpConfig.readDirToolName: 56 | return try XcfMcpFileSystemHandlers.handleReadDirToolCall(params) 57 | 58 | case McpConfig.closeDocToolName: 59 | return try XcfMcpXcodeDocumentHandlers.handleCloseDocToolCall(params) 60 | 61 | case McpConfig.tools: 62 | return XcfMcpHelpHandlers.handleToolsReferenceToolCall(params) 63 | 64 | case McpConfig.xcfHelp: 65 | return XcfMcpHelpHandlers.handleQuickHelpToolCall(params) 66 | 67 | case McpConfig.createDiff: 68 | return try XcfMcpDiffHandlers.handleCreateDiffToolCall(params) 69 | 70 | case McpConfig.applyDiff: 71 | return try XcfMcpDiffHandlers.handleApplyDiffToolCall(params) 72 | 73 | case McpConfig.getAsciiDiff: 74 | return try XcfMcpDiffHandlers.handleGetAsciiDiffToolCall(params) 75 | 76 | case McpConfig.createDiffFromDocToolName: 77 | return try XcfMcpDiffHandlers.handleCreateDiffFromDocToolCall(params) 78 | 79 | case McpConfig.applyUndoDiffToDocToolName: 80 | return try XcfMcpDiffHandlers.handleApplyUndoDiffToDocToolCall(params) 81 | 82 | case McpConfig.applyDiffToDocToolName: 83 | return try XcfMcpDiffHandlers.handleApplyDiffToDocToolCall(params) 84 | 85 | // Action-specific tool handlers - delegate to specialized handler 86 | case McpConfig.showHelpToolName: 87 | return await XcfMcpActionHandlers.handleShowHelpToolCall(params) 88 | 89 | case McpConfig.grantPermissionToolName: 90 | return await XcfMcpActionHandlers.handleGrantPermissionToolCall(params) 91 | 92 | case McpConfig.runProjectToolName: 93 | return await XcfMcpActionHandlers.handleRunProjectToolCall(params) 94 | 95 | case McpConfig.buildProjectToolName: 96 | return await XcfMcpActionHandlers.handleBuildProjectToolCall(params) 97 | 98 | case McpConfig.showCurrentProjectToolName: 99 | return await XcfMcpActionHandlers.handleShowCurrentProjectToolCall(params) 100 | 101 | case McpConfig.showEnvToolName: 102 | return await XcfMcpActionHandlers.handleShowEnvToolCall(params) 103 | 104 | case McpConfig.showFolderToolName: 105 | return await XcfMcpActionHandlers.handleShowFolderToolCall(params) 106 | 107 | case McpConfig.listProjectsToolName: 108 | return await XcfMcpActionHandlers.handleListProjectsToolCall(params) 109 | 110 | case McpConfig.selectProjectToolName: 111 | return await XcfMcpActionHandlers.handleSelectProjectToolCall(params) 112 | 113 | case McpConfig.analyzeSwiftCodeToolName: 114 | return await XcfMcpActionHandlers.handleAnalyzeSwiftCodeToolCall(params) 115 | 116 | default: 117 | throw MCPError.invalidParams(String(format: ErrorMessages.unknownTool, params.name)) 118 | } 119 | } 120 | 121 | // MARK: - Core Tool Handler 122 | 123 | /// Handles a call to the xcf tool 124 | static func handleXcfToolCall(_ params: CallTool.Parameters) async throws -> CallTool.Result { 125 | if let action = params.arguments?[McpConfig.actionParamName]?.stringValue { 126 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.handleAction(action: action))]) 127 | } else { 128 | // If no action specified, return the help information 129 | return CallTool.Result(content: [.text(await XcfMcpActionHandler.handleAction(action: Actions.help))]) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /xcf/ORGANIZATION_SUMMARY.md: -------------------------------------------------------------------------------- 1 | # XCF MCP Organization Summary 2 | 3 | ## New Organization (No Extensions!) 4 | 5 | The codebase has been reorganized into focused, single-responsibility classes without using Swift extensions. 6 | 7 | ### File Structure 8 | 9 | 1. **`XcfMcpServer.swift`** - Core server setup and initialization 10 | 2. **`XcfMcpTools.swift`** - All tool definitions (30+ tools) 11 | 3. **`XcfMcpResources.swift`** - All resource definitions (10 resources) 12 | 4. **`XcfMcpPrompts.swift`** - All prompt definitions (20+ prompts) 13 | 5. **`XcfMcpHandlers.swift`** - Main handler registration and utility methods 14 | 6. **`XcfMcpToolCallHandlers.swift`** - Main tool call router and core tool handlers 15 | 7. **`XcfMcpResourceHandlers.swift`** - Resource request handlers (Xcode projects, build results, file contents) 16 | 8. **`XcfMcpPromptHandlers.swift`** - Prompt request handlers (build, run, analyze code) 17 | 9. **`XcfMcpHelpHandlers.swift`** - Help and documentation tool handlers 18 | 10. **`XcfMcpFileSystemHandlers.swift`** - File system operation handlers 19 | 11. **`XcfMcpXcodeDocumentHandlers.swift`** - Xcode document operation handlers 20 | 12. **`XcfMcpCodeAnalysisHandlers.swift`** - Code snippet and analyzer handlers 21 | 13. **`XcfMcpCodeSnippetHandlers.swift`** - Code snippet extraction and analysis 22 | 14. **`XcfMcpDiffHandlers.swift`** - Diff creation and application handlers 23 | 15. **`XcfMcpActionHandlers.swift`** - Action-specific tool handlers (project, environment, etc.) 24 | 16. **`XcfMcpDefinitions.swift`** - Simple coordinator that delegates to specialized classes 25 | 17. **`XcfMcpCollections.swift`** - Legacy compatibility layer (can be removed later) 26 | 18. **`XcfMcpConfig.swift`** - MCP-specific configuration constants 27 | 19. **`XcfMcpStructStrings.swift`** - String constants and message structs 28 | 29 | ### Benefits of This Organization 30 | 31 | - **No Extensions**: Uses plain classes with static methods 32 | - **Single Responsibility**: Each file has one clear purpose 33 | - **Easy to Find**: Tools, resources, prompts, and handlers are in separate files by category 34 | - **Maintainable**: Adding new functionality is straightforward 35 | - **Clean Dependencies**: Clear hierarchy and delegation patterns 36 | - **Focused Files**: Each handler file focuses on one type of functionality 37 | - **Separated Constants**: String constants are now in their own dedicated file 38 | - **Complete Separation**: Resources and prompts have their own dedicated handler files 39 | 40 | ### Architecture Flow 41 | 42 | ``` 43 | XcfMcpServer 44 | └── XcfMcpHandlers (registration + utilities) 45 | ├── XcfMcpToolCallHandlers (main tool router) 46 | │ ├── XcfMcpHelpHandlers (help & docs) 47 | │ ├── XcfMcpFileSystemHandlers (file ops) 48 | │ ├── XcfMcpXcodeDocumentHandlers (xcode docs) 49 | │ ├── XcfMcpCodeAnalysisHandlers (code analysis) 50 | │ │ └── XcfMcpCodeSnippetHandlers (snippet extraction) 51 | │ ├── XcfMcpDiffHandlers (diff ops) 52 | │ └── XcfMcpActionHandlers (project/env actions) 53 | ├── XcfMcpResourceHandlers (resource handling) 54 | ├── XcfMcpPromptHandlers (prompt handling) 55 | ├── XcfMcpTools (tool definitions) 56 | ├── XcfMcpResources (resource definitions) 57 | ├── XcfMcpPrompts (prompt definitions) 58 | ├── XcfMcpConfig (mcp config constants) 59 | └── XcfMcpStructStrings (string constants) 60 | ``` 61 | 62 | ### How to Add New Items 63 | 64 | - **New Tool**: Add to `XcfMcpTools.swift` 65 | - **New Resource**: Add to `XcfMcpResources.swift` 66 | - **New Prompt**: Add to `XcfMcpPrompts.swift` 67 | - **New Help Handler**: Add to `XcfMcpHelpHandlers.swift` 68 | - **New File System Handler**: Add to `XcfMcpFileSystemHandlers.swift` 69 | - **New Xcode Document Handler**: Add to `XcfMcpXcodeDocumentHandlers.swift` 70 | - **New Code Analysis Handler**: Add to `XcfMcpCodeAnalysisHandlers.swift` 71 | - **New Code Snippet Handler**: Add to `XcfMcpCodeSnippetHandlers.swift` 72 | - **New Diff Handler**: Add to `XcfMcpDiffHandlers.swift` 73 | - **New Action Handler**: Add to `XcfMcpActionHandlers.swift` 74 | - **New Resource Handler**: Add to `XcfMcpResourceHandlers.swift` 75 | - **New Prompt Handler**: Add to `XcfMcpPromptHandlers.swift` 76 | - **New MCP Config**: Add to `XcfMcpConfig.swift` 77 | - **New String Constant**: Add to `XcfMcpStructStrings.swift` 78 | 79 | ### Handler Categories 80 | 81 | The handlers are now organized into logical categories: 82 | 83 | - **`XcfMcpHelpHandlers.swift`**: Help, documentation, and reference tools 84 | - **`XcfMcpFileSystemHandlers.swift`**: File reading, directory operations 85 | - **`XcfMcpXcodeDocumentHandlers.swift`**: Xcode document management (open, create, read, save, close) 86 | - **`XcfMcpCodeAnalysisHandlers.swift`**: Code snippet and analyzer tool coordination 87 | - **`XcfMcpCodeSnippetHandlers.swift`**: Code extraction and analysis logic 88 | - **`XcfMcpDiffHandlers.swift`**: Diff creation and application 89 | - **`XcfMcpActionHandlers.swift`**: Action-specific tools (project management, environment, build, run) 90 | - **`XcfMcpResourceHandlers.swift`**: Resource operations (Xcode projects, build results, file contents) 91 | - **`XcfMcpPromptHandlers.swift`**: Prompt processing (build, run, analyze code prompts) 92 | 93 | ### Configuration and Constants 94 | 95 | The configuration is now properly separated: 96 | 97 | - **`XcfMcpConfig.swift`**: MCP-specific configuration (tools, resources, prompts, schema definitions) 98 | - **`XcfMcpStructStrings.swift`**: String constants (error messages, success messages, app constants, actions, paths, format, Xcode constants) 99 | 100 | ### Migration Notes 101 | 102 | The old extension-based files have been removed and replaced with this focused structure: 103 | - ~~`XcfMcpToolCall.swift`~~ → split into multiple specialized handler files 104 | - ~~`XcfMcpRegistration.swift`~~ → merged into `XcfMcpHandlers.swift` 105 | - ~~`XcfMcpToolParams.swift`~~ → split into `XcfMcpTools.swift` 106 | 107 | Individual tool handlers are now distributed across focused files by functionality, and constants are properly separated by purpose. The codebase now has **19 perfectly organized files** with complete separation of concerns, making it much easier to navigate and maintain! -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpDiffHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | import MultiLineDiff 4 | 5 | // MARK: - Diff Tool Handlers 6 | 7 | class XcfMcpDiffHandlers { 8 | 9 | /// Handles a call to create a diff for a document 10 | static func handleCreateDiffToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 11 | guard let arguments = params.arguments else { 12 | throw MCPError.invalidParams("Missing Params, fix later") 13 | } 14 | 15 | guard let destString = arguments["destString"]?.stringValue else { 16 | return CallTool.Result(content: [.text("Missing destinationString")]) 17 | } 18 | 19 | guard let sourceString = arguments["sourceString"]?.stringValue else { 20 | return CallTool.Result(content: [.text("Missing sourceString")]) 21 | } 22 | 23 | do { 24 | let diffHash = try createDiffFromString(original: sourceString, modified: destString) 25 | return CallTool.Result(content: [.text(diffHash)]) 26 | 27 | } catch { 28 | throw MCPError.invalidParams(String(format: ErrorMessages.errorCreatingDiff, error.localizedDescription)) 29 | } 30 | } 31 | 32 | /// Handles a call to the apply_diff tool 33 | static func handleGetAsciiDiffToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 34 | guard let arguments = params.arguments else { 35 | return CallTool.Result(content: [.text("I dunno, not done yet.")]) 36 | } 37 | 38 | guard let diffHash = arguments["diffHash"]?.stringValue else { 39 | return CallTool.Result(content: [.text("Missing diff hash")]) 40 | } 41 | 42 | do { 43 | var diff = try getAsciiDiffFromHash(diffHash: diffHash) 44 | if diff.isEmpty { 45 | diff = "Having Issues, \(diffHash)" 46 | } 47 | return CallTool.Result(content: [.text(diff )]) 48 | 49 | } catch { 50 | throw MCPError.invalidParams(String(format: ErrorMessages.errorCreatingDiff, error.localizedDescription)) 51 | } 52 | } 53 | 54 | /// Handles a call to the apply_diff tool 55 | static func handleApplyDiffToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 56 | guard let arguments = params.arguments else { 57 | return CallTool.Result(content: [.text("I dunno, not done yet.")]) 58 | } 59 | 60 | guard let diffHash = arguments["diffHash"]?.stringValue else { 61 | return CallTool.Result(content: [.text("Missing diff hash")]) 62 | } 63 | 64 | guard let sourceString = arguments["sourceString"]?.stringValue else { 65 | return CallTool.Result(content: [.text("Missing sourceString")]) 66 | } 67 | 68 | do { 69 | var diff = try applyDiffFromString(original: sourceString, diffHash: diffHash) 70 | if diff.isEmpty { 71 | diff = "Having Issues, \(sourceString) \(diffHash)" 72 | } 73 | return CallTool.Result(content: [.text(diff )]) 74 | 75 | } catch { 76 | throw MCPError.invalidParams(String(format: ErrorMessages.errorCreatingDiff, error.localizedDescription)) 77 | } 78 | } 79 | 80 | /// Handles a call to create a diff from a document 81 | static func handleCreateDiffFromDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 82 | guard let arguments = params.arguments else { 83 | throw MCPError.invalidParams("Missing arguments") 84 | } 85 | 86 | guard let filePath = arguments[McpConfig.filePathParamName]?.stringValue else { 87 | return CallTool.Result(content: [.text("Missing filePath parameter")]) 88 | } 89 | 90 | guard let modifiedContent = arguments[McpConfig.modifiedContentParamName]?.stringValue else { 91 | return CallTool.Result(content: [.text("Missing modifiedContent parameter")]) 92 | } 93 | 94 | do { 95 | let diffHash = try createDiffFromDocument(filePath: filePath, modifiedContent: modifiedContent) 96 | return CallTool.Result(content: [.text(diffHash)]) 97 | } catch { 98 | throw MCPError.invalidParams(String(format: "Error creating diff from document: %@", error.localizedDescription)) 99 | } 100 | } 101 | 102 | /// Handles a call to apply a diff to a document 103 | static func handleApplyDiffToDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 104 | guard let arguments = params.arguments else { 105 | throw MCPError.invalidParams("Missing arguments") 106 | } 107 | 108 | guard let filePath = arguments[McpConfig.filePathParamName]?.stringValue else { 109 | return CallTool.Result(content: [.text("Missing filePath parameter")]) 110 | } 111 | 112 | guard let diffHash = arguments[McpConfig.diffHashParamName]?.stringValue else { 113 | return CallTool.Result(content: [.text("Missing diffHash parameter")]) 114 | } 115 | 116 | do { 117 | let diffResult = try getDiffResultFromHash(diffHash: diffHash) 118 | let success = try applyDiffToDocument(filePath: filePath, operations: diffResult) 119 | if success { 120 | return CallTool.Result(content: [.text("Diff applied successfully to document: \(filePath)")]) 121 | } else { 122 | return CallTool.Result(content: [.text("Failed to apply diff to document: \(filePath)")]) 123 | } 124 | } catch { 125 | throw MCPError.invalidParams(String(format: "Error applying diff to document: %@", error.localizedDescription)) 126 | } 127 | } 128 | 129 | /// Handles a call to apply a diff to a document 130 | static func handleApplyUndoDiffToDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 131 | guard let arguments = params.arguments else { 132 | throw MCPError.invalidParams("Missing arguments") 133 | } 134 | 135 | guard let filePath = arguments[McpConfig.filePathParamName]?.stringValue else { 136 | return CallTool.Result(content: [.text("Missing filePath parameter")]) 137 | } 138 | 139 | guard let diffHash = arguments[McpConfig.diffHashParamName]?.stringValue else { 140 | return CallTool.Result(content: [.text("Missing diffHash parameter")]) 141 | } 142 | 143 | do { 144 | let diffResult = try getDiffResultFromHash(diffHash: diffHash) 145 | let success = try applyDiffToDocument(filePath: filePath, operations: diffResult) 146 | if success { 147 | return CallTool.Result(content: [.text("Diff applied successfully to document: \(filePath)")]) 148 | } else { 149 | return CallTool.Result(content: [.text("Failed to apply diff to document: \(filePath)")]) 150 | } 151 | } catch { 152 | throw MCPError.invalidParams(String(format: "Error applying diff to document: %@", error.localizedDescription)) 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/XcfMcpXcodeDocumentHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - Xcode Document Tool Handlers 5 | 6 | class XcfMcpXcodeDocumentHandlers { 7 | 8 | /// Handles opening a document in Xcode 9 | static func handleOpenDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 10 | guard let arguments = params.arguments else { 11 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 12 | } 13 | 14 | // Try to get filePath from arguments in two ways: 15 | // 1. As a named parameter (filePath=...) 16 | // 2. As a direct argument (first argument after command) 17 | let filePath: String 18 | if let namedPath = arguments[McpConfig.filePathParamName]?.stringValue { 19 | filePath = namedPath 20 | } else if let firstArg = arguments.first?.value.stringValue { 21 | filePath = firstArg 22 | } else { 23 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 24 | } 25 | 26 | // Use FuzzyLogicService to resolve the path for better error messages 27 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 28 | 29 | // If there was a warning, include it in the response 30 | var responseMessage = "" 31 | if !warning.isEmpty { 32 | responseMessage += "Warning: \(warning)\n" 33 | } 34 | 35 | if XcfMcpServer.XcfScript.openSwiftDocument(filePath: resolvedPath) { 36 | responseMessage += McpConfig.documentOpenedSuccessfully 37 | return CallTool.Result(content: [.text(responseMessage)]) 38 | } else { 39 | // Check if file exists to provide more specific error 40 | if !FileManager.default.fileExists(atPath: resolvedPath) { 41 | responseMessage += "Error: File does not exist: \(resolvedPath)" 42 | } else if !FileManager.default.isReadableFile(atPath: resolvedPath) { 43 | responseMessage += "Error: File is not readable: \(resolvedPath)" 44 | } else { 45 | responseMessage += String(format: ErrorMessages.errorOpeningFile, resolvedPath) 46 | } 47 | return CallTool.Result(content: [.text(responseMessage)]) 48 | } 49 | } 50 | 51 | /// Handles creating a document in Xcode 52 | static func handleCreateDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 53 | guard let arguments = params.arguments, 54 | let filePath = arguments[McpConfig.filePathParamName]?.stringValue else { 55 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 56 | } 57 | 58 | let content = arguments[McpConfig.contentParamName]?.stringValue ?? "" 59 | 60 | if XcfMcpServer.XcfScript.createSwiftDocumentWithFileManager(filePath: filePath, content: content) { 61 | return CallTool.Result(content: [.text(McpConfig.documentCreatedSuccessfully)]) 62 | } else { 63 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorCreatingFile, filePath))]) 64 | } 65 | } 66 | 67 | /// Handles reading a document from Xcode 68 | static func handleReadDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 69 | guard let arguments = params.arguments else { 70 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 71 | } 72 | 73 | // Try to get filePath from arguments in two ways: 74 | // 1. As a named parameter (filePath=...) 75 | // 2. As a direct argument (first argument after command) 76 | let filePath: String 77 | if let namedPath = arguments[McpConfig.filePathParamName]?.stringValue { 78 | filePath = namedPath 79 | } else if let firstArg = arguments.first?.value.stringValue { 80 | filePath = firstArg 81 | } else { 82 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 83 | } 84 | 85 | // Use FuzzyLogicService to resolve the path 86 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 87 | 88 | if let content = XcfMcpServer.XcfScript.readSwiftDocumentWithFileManager(filePath: resolvedPath) { 89 | return CallTool.Result(content: [.text(content)]) 90 | } else { 91 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorReadingFile, filePath))]) 92 | } 93 | } 94 | 95 | /// Handles saving a document in Xcode 96 | static func handleSaveDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 97 | guard let arguments = params.arguments, 98 | let filePath = arguments[McpConfig.filePathParamName]?.stringValue else { 99 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 100 | } 101 | 102 | if XcfMcpServer.XcfScript.writeSwiftDocumentWithFileManager(filePath: filePath, content: "") { 103 | return CallTool.Result(content: [.text(McpConfig.documentSavedSuccessfully)]) 104 | } else { 105 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorWritingFile, filePath))]) 106 | } 107 | } 108 | 109 | /// Handles closing a document in Xcode 110 | static func handleCloseDocToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 111 | // Debug: Print out the entire params 112 | 113 | guard let arguments = params.arguments else { 114 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 115 | } 116 | 117 | // Try to get filePath from arguments in two ways: 118 | // 1. As a named parameter (filePath=...) 119 | // 2. As a direct argument (first argument after command) 120 | let filePath: String 121 | if let namedPath = arguments[McpConfig.filePathParamName]?.stringValue { 122 | filePath = namedPath 123 | } else if let firstArg = arguments.first?.value.stringValue { 124 | filePath = firstArg 125 | } else { 126 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 127 | } 128 | 129 | // Get saving option, handling both boolean and string representations 130 | let savingParam: Bool 131 | if let boolValue = arguments["saving"]?.boolValue { 132 | savingParam = boolValue 133 | } else if let stringValue = arguments["saving"]?.stringValue { 134 | // Handle string representations of boolean 135 | savingParam = ["true", "yes", "1"].contains(stringValue.lowercased()) 136 | } else { 137 | return CallTool.Result(content: [.text("Missing 'saving' parameter")]) 138 | } 139 | 140 | // Convert boolean to XcodeSaveOptions 141 | let saveOptions: XcodeSaveOptions = savingParam ? .yes : .no 142 | 143 | if XcfMcpServer.XcfScript.closeSwiftDocument(filePath: filePath, xcSaveOptions: saveOptions) { 144 | return CallTool.Result(content: [.text(McpConfig.documentClosedSuccessfully)]) 145 | } else { 146 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorClosingFile, filePath))]) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /xcf/Mcp/XcDoc/XcfXcodeProjectManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfXcodeProjectManager.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/4/25. 6 | // 7 | 8 | import Foundation 9 | 10 | // Environment variable keys 11 | private enum EnvKeys { 12 | static let xcodeProject = EnvVarKeys.xcodeProject 13 | static let workspaceFolderPaths = EnvVarKeys.workspaceFolderPaths 14 | static let xcodeProjectFolder = EnvVarKeys.xcodeProjectFolder 15 | static let xcodeProjectPath = EnvVarKeys.xcodeProjectPath 16 | } 17 | 18 | // File extensions 19 | private enum FileExtensions { 20 | static let xcodeproj = Format.xcodeProjExtension 21 | static let xcworkspace = Format.xcodeWorkExtension 22 | 23 | static func isValidXcodeProjectPath(_ path: String) -> Bool { 24 | path.hasSuffix(xcodeproj) || path.hasSuffix(xcworkspace) 25 | } 26 | 27 | static func removeProjectSuffix(from path: String) -> String { 28 | if path.hasSuffix(xcodeproj) { 29 | return String(path.dropLast(xcodeproj.count)) 30 | } else if path.hasSuffix(xcworkspace) { 31 | return String(path.dropLast(xcworkspace.count)) 32 | } 33 | return path 34 | } 35 | } 36 | 37 | // Global variables with clear prefix for backward compatibility 38 | private var XcfXcodeProject: String? = { 39 | if let workspaceProjectPath = ProcessInfo.processInfo.environment[EnvKeys.workspaceFolderPaths], 40 | !workspaceProjectPath.contains(Format.commaSeparator) { 41 | let projectName = URL(fileURLWithPath: workspaceProjectPath).lastPathComponent 42 | let constructedProjectPath = workspaceProjectPath + "/" + projectName + ".xcodeproj" 43 | 44 | // Validate the constructed path exists before returning 45 | if FileManager.default.fileExists(atPath: constructedProjectPath) { 46 | return constructedProjectPath 47 | } 48 | 49 | // Fallback if constructed path doesn't exist 50 | return ProcessInfo.processInfo.environment[EnvKeys.xcodeProject] ?? 51 | XcfSwiftScript.shared.activeWorkspacePath() 52 | } 53 | 54 | // Original fallback 55 | return ProcessInfo.processInfo.environment[EnvKeys.xcodeProject] ?? 56 | XcfSwiftScript.shared.activeWorkspacePath() 57 | }() 58 | 59 | private var XcfXcodeFolder: String? = { 60 | if let workspaceFolderPath = ProcessInfo.processInfo.environment[EnvKeys.workspaceFolderPaths], 61 | !workspaceFolderPath.contains(",") { 62 | let projectName = URL(fileURLWithPath: workspaceFolderPath).lastPathComponent 63 | let constructedFolderPath = workspaceFolderPath + "/" + projectName 64 | 65 | // Validate the constructed path exists before returning 66 | if FileManager.default.fileExists(atPath: constructedFolderPath) { 67 | return constructedFolderPath 68 | } 69 | 70 | // Fallback if constructed path doesn't exist 71 | return ProcessInfo.processInfo.environment[EnvKeys.xcodeProjectFolder] ?? 72 | ProcessInfo.processInfo.environment[EnvKeys.xcodeProjectPath] 73 | } 74 | 75 | // Original fallback 76 | return ProcessInfo.processInfo.environment[EnvKeys.xcodeProjectFolder] ?? 77 | ProcessInfo.processInfo.environment[EnvKeys.xcodeProjectPath] 78 | }() 79 | 80 | /// Centralized manager for Xcode project and folder paths 81 | class XcfXcodeProjectManager { 82 | // Singleton instance 83 | static let shared = XcfXcodeProjectManager() 84 | 85 | private init() {} 86 | 87 | // MARK: - Properties 88 | 89 | /// Get the current Xcode project path 90 | var currentProject: String? { 91 | get { return XcfXcodeProject } 92 | set { XcfXcodeProject = newValue } 93 | } 94 | 95 | /// Get the current working folder path 96 | var currentFolder: String? { 97 | get { return XcfXcodeFolder } 98 | set { XcfXcodeFolder = newValue } 99 | } 100 | 101 | // MARK: - Public Methods 102 | 103 | /// Update both project and folder paths based on project selection 104 | /// - Parameter projectPath: The path to the selected Xcode project 105 | func updateFromProjectSelection(_ projectPath: String) { 106 | guard FileExtensions.isValidXcodeProjectPath(projectPath) else { 107 | return 108 | } 109 | 110 | currentProject = projectPath 111 | currentFolder = FileExtensions.removeProjectSuffix(from: projectPath) 112 | 113 | updateWorkingDirectory() 114 | } 115 | 116 | /// Initializes the manager with values from environment variables 117 | func initialize() { 118 | initializeProject() 119 | initializeFolder() 120 | updateWorkingDirectory() 121 | } 122 | 123 | /// Check if the project is within the allowed workspace folder 124 | /// - Parameters: 125 | /// - projectPath: The path to check 126 | /// - workspaceFolder: The workspace folder to check against 127 | /// - Returns: True if the project is within the workspace folder 128 | func isProjectInWorkspaceFolder(projectPath: String, workspaceFolder: String) -> Bool { 129 | let projectURL = URL(fileURLWithPath: projectPath) 130 | let workspaceFolderURL = URL(fileURLWithPath: workspaceFolder) 131 | return projectURL.path.contains(workspaceFolderURL.path) 132 | } 133 | 134 | // MARK: - Private Methods 135 | 136 | private func initializeProject() { 137 | if currentProject == nil { 138 | let potentialProject = ProcessInfo.processInfo.environment[EnvKeys.xcodeProject] ?? XcfSwiftScript.shared.activeWorkspacePath() 139 | 140 | if let projectPath = potentialProject, 141 | FileExtensions.isValidXcodeProjectPath(projectPath) { 142 | currentProject = projectPath 143 | } 144 | } 145 | } 146 | 147 | private func initializeFolder() { 148 | if currentFolder == nil { 149 | if let paths = ProcessInfo.processInfo.environment[EnvKeys.workspaceFolderPaths] { 150 | // Split by comma and take the first path 151 | let components = paths.components(separatedBy: Format.commaSeparator) 152 | currentFolder = components.first?.trimmingCharacters(in: .whitespaces) 153 | } else if let folderPath = ProcessInfo.processInfo.environment[EnvKeys.xcodeProjectFolder] ?? 154 | ProcessInfo.processInfo.environment[EnvKeys.xcodeProjectPath] { 155 | currentFolder = folderPath 156 | } else if let projectPath = currentProject { 157 | currentFolder = FileExtensions.removeProjectSuffix(from: projectPath) 158 | } 159 | } 160 | } 161 | 162 | private func updateWorkingDirectory() { 163 | guard let folderPath = currentFolder else { return } 164 | 165 | do { 166 | try XcfFileManager.changeDirectory(to: folderPath) 167 | } catch { 168 | print(String(format: ErrorMessages.errorChangingDirectory, error.localizedDescription)) 169 | } 170 | } 171 | } 172 | 173 | // For backward compatibility - consider deprecating in future versions 174 | var currentProject: String? { 175 | get { return XcfXcodeProjectManager.shared.currentProject } 176 | set { XcfXcodeProjectManager.shared.currentProject = newValue } 177 | } 178 | 179 | var currentFolder: String? { 180 | get { return XcfXcodeProjectManager.shared.currentFolder } 181 | set { XcfXcodeProjectManager.shared.currentFolder = newValue } 182 | } 183 | -------------------------------------------------------------------------------- /xcf/Mcp/ServerConfig/XcfMcpConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfMcpStructStrings.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/17/25. 6 | // 7 | import Foundation 8 | 9 | // Core app constants 10 | struct AppConstants { 11 | static let appName = "xcf" 12 | } 13 | 14 | // Define string constants for commands 15 | struct Actions { 16 | static let xcf = AppConstants.appName, 17 | help = "help", 18 | show = "show", 19 | open = "open", 20 | run = "run", 21 | build = "build", 22 | grant = "grant", 23 | current = "current", 24 | env = "env", 25 | pwd = "pwd", 26 | dir = "dir", 27 | path = "path", 28 | analyze = "analyze", 29 | lz = "lz" // Short alias for analyze 30 | } 31 | 32 | // Environment variable constants 33 | struct EnvVarKeys { 34 | static let xcodeProject = "XCODE_PROJECT", 35 | workspaceFolderPaths = "WORKSPACE_FOLDER_PATHS", 36 | xcodeProjectFolder = "XCODE_PROJECT_FOLDER", 37 | xcodeProjectPath = "XCODE_PROJECT_PATH" 38 | } 39 | 40 | // Define error messages 41 | struct ErrorMessages { 42 | // MARK: - Project Selection Errors 43 | static let noProjectSelected = "No project selected yet. Use 'show' to see available projects.", 44 | noOpenProjects = "I don't see any open Xcode projects. Try opening one first.", 45 | invalidProjectSelection = "That's not a valid selection. Try 'open 1' to select the first project.", 46 | projectOutOfRange = "Project %@ doesn't exist. I only found %@ projects.", 47 | noProjectInWorkspace = "I couldn't find a project in your workspace.", 48 | invalidProjectPath = "Warning: %@ is not a valid Xcode project or workspace path" 49 | 50 | // MARK: - Action Errors 51 | static let unrecognizedAction = "I don't understand '%@'. Try 'help' to see what I can do.", 52 | toolCallError = "Error executing tool '%@': %@" 53 | 54 | // MARK: - Xcode Connection Errors 55 | static let failedToConnectXcode = "I couldn't connect to Xcode. Is it running?", 56 | noWorkspaceFound = "I couldn't find a workspace document. Try opening an Xcode project first." 57 | 58 | // MARK: - Build Errors 59 | static let failedToStartBuild = "I had trouble starting the build. Please try again.", 60 | failedToGetBuildResult = "I couldn't get the build results. Something went wrong.", 61 | errorGettingBuildResults = "Error getting build results: %@" 62 | 63 | // MARK: - File Operation Errors 64 | static let errorReadingFile = "Error reading file: %@", 65 | errorWritingFile = "Error writing file: %@", 66 | errorCreatingFile = "Error creating file: %@", 67 | errorEditingFile = "Error editing file: %@", 68 | errorDeletingFile = "Error deleting file: %@", 69 | errorOpeningFile = "Error opening file: %@", 70 | errorClosingFile = "Error closing file: %@", 71 | errorReadingDirectory = "Error reading directory: %@", 72 | errorCreatingDirectory = "Error creating directory: %@", 73 | errorSelectingDirectory = "Error selecting directory: %@", 74 | errorListingProjects = "Error listing projects: %@", 75 | fileNotFound = "File not found: %@", 76 | directoryNotFound = "Directory not found: %@", 77 | invalidFilePath = "Invalid file path: %@", 78 | invalidDirectoryPath = "Invalid directory path: %@", 79 | fileAlreadyExists = "File already exists: %@", 80 | directoryAlreadyExists = "Directory already exists: %@", 81 | permissionDenied = "Permission denied: %@", 82 | unknownFileError = "Unknown error occurred while operating on: %@" 83 | 84 | // MARK: - Osascript Errors 85 | static let failedToConvertOutput = "Failed to convert output data to string.", 86 | failedToExecuteOsascript = "Failed to execute osascript: %@", 87 | failedToCreateAppleScript = "Failed to create AppleScript.", 88 | appleScriptError = "Error: %@" 89 | 90 | // MARK: - MCP Errors 91 | static let unknownTool = "Unknown tool: %@" 92 | 93 | // MARK: - Code Snippet Errors 94 | static let missingLineParamsError = "Missing required line parameters when entireFile is false", 95 | missingFilePathError = "Missing required filePath parameter", 96 | invalidLineNumbers = "Those line numbers don't look right. Please check them." 97 | 98 | // Resource error messages 99 | static let missingFilePathParamError = "Missing required filePath parameter for file operation", 100 | missingDirectoryPathParamError = "Missing required directoryPath parameter", 101 | unknownResourceUriError = "Unknown resource URI: %@", 102 | unknownPromptNameError = "Unknown prompt name: %@" 103 | 104 | // Directory operation errors 105 | static let errorChangingDirectory = "Error changing directory: %@", 106 | errorRemovingDirectory = "Error removing directory: %@" 107 | 108 | // Error message for creating a diff 109 | static let errorCreatingDiff = "Error creating diff: %@" 110 | } 111 | 112 | // Define success messages 113 | struct SuccessMessages { 114 | static let xcfActive = "All \(AppConstants.appName) systems go!", 115 | buildSuccess = "🐦📜 Built successfully", 116 | runSuccess = "🐦📜 Ran successfully", 117 | permissionGranted = "Permission Granted", 118 | success = "success", 119 | pwdSuccess = "Current directory: %@", 120 | currentProject = "%@", 121 | environmentVariables = "Environment Variables: %@", 122 | securityPreventManualSelection = "Staying safe! I've kept you in your workspace.\nYour workspace: %@\nUsing: %@" 123 | } 124 | 125 | // Define path constants 126 | struct Paths { 127 | static let osascriptPath = "/usr/bin/osascript" 128 | } 129 | 130 | // Define file extensions and formats 131 | struct Format { 132 | static let xcodeProjExtension = ".xcodeproj", 133 | xcodeWorkExtension = ".xcworkspace", 134 | projectListFormat = "%d. %@\n", 135 | newLine = "\n", 136 | spaceSeparator = " ", 137 | commaSeparator = "," 138 | 139 | // Regex patterns 140 | static let quoteExtractPattern = /\"([^\"]+)\"/ 141 | 142 | // Character sets 143 | static func newlinesCharSet() -> CharacterSet { 144 | return .newlines 145 | } 146 | } 147 | 148 | // Define Xcode-related constants 149 | struct XcodeConstants { 150 | // Bundle IDs 151 | static let xcodeBundleIdentifier = "com.apple.dt.Xcode" 152 | 153 | // Issue types 154 | static let errorIssueType = "Error", 155 | warningIssueType = "Warning", 156 | analyzerIssueType = "Analyzer Issue", 157 | testFailureIssueType = "Test Failure" 158 | 159 | // Time intervals 160 | static let buildPollInterval = 0.5, // seconds 161 | runDelayInterval: UInt32 = 1 // seconds 162 | 163 | // File prefix and format 164 | static let filePrefix = "File:`", 165 | fileSuffix = "`:" 166 | 167 | // Code formatting 168 | static let codeBlockStart = "```", 169 | codeBlockEnd = "```", 170 | issueFormat = "%@:%d:%d [%@] %@" 171 | } 172 | -------------------------------------------------------------------------------- /xcf/Mcp/Script/XcfSwiftDiff.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfSwiftDiff.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/19/25. 6 | // 7 | 8 | import Foundation 9 | import MultiLineDiff 10 | 11 | var DiffOperationDict: [String : DiffResult] = [:] 12 | 13 | func createDiff(original: String, modified: String) -> DiffResult { 14 | MultiLineDiff.createDiff(source: original, destination: modified) 15 | } 16 | 17 | func applyDiff(original: String, diff: DiffResult) throws -> String { 18 | try MultiLineDiff.applyDiff(to: original, diff: diff) 19 | } 20 | 21 | /// Applies a diff to a document 22 | /// - Parameters: 23 | /// - filePath: Path to the source document 24 | /// - operations: The diff operations to apply 25 | /// - Returns: True if successful, false otherwise 26 | /// - Throws: File access, parsing, or application errors 27 | func applyUndoDiffToDocument( 28 | filePath: String, 29 | operations: DiffResult 30 | ) throws -> Bool { 31 | // Resolve the file path 32 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 33 | 34 | // Validate file exists 35 | guard FileManager.default.fileExists(atPath: resolvedPath) else { 36 | throw NSError(domain: "XcfSwiftDiff", code: 1, userInfo: [ 37 | NSLocalizedDescriptionKey: "File not found: \(filePath)" 38 | ]) 39 | } 40 | 41 | // Read the original content using ScriptingBridge 42 | guard let originalContent = XcfSwiftScript.shared.readSwiftDocumentWithFileManager(filePath: resolvedPath) else { 43 | throw NSError(domain: "XcfSwiftDiff", code: 3, userInfo: [ 44 | NSLocalizedDescriptionKey: "Failed to read document content" 45 | ]) 46 | } 47 | 48 | // Apply the diff to the entire content using the existing applyDiff function 49 | guard let operations = MultiLineDiff.createUndoDiff(from: operations) else { 50 | return false 51 | } 52 | 53 | let modifiedContent = try applyDiff(original: originalContent, diff: operations) 54 | 55 | // Write the modified content back to the file using ScriptingBridge 56 | if !XcfSwiftScript.shared.writeSwiftDocumentWithFileManager(filePath: resolvedPath, content: modifiedContent) { 57 | throw NSError(domain: "XcfSwiftDiff", code: 4, userInfo: [ 58 | NSLocalizedDescriptionKey: "Failed to write modified content to document" 59 | ]) 60 | } 61 | 62 | return true 63 | } 64 | 65 | func getAsciiDiffFromHash(diffHash: String) throws -> String { 66 | // Try to retrieve diff operations from the dictionary 67 | guard let storedOperations = DiffOperationDict[diffHash] else { 68 | throw NSError(domain: "XcfSwiftDiff", code: 5, userInfo: [ 69 | NSLocalizedDescriptionKey: "No diff operations found for the given hash" 70 | ]) 71 | } 72 | 73 | guard let source = storedOperations.metadata?.sourceContent else { return "bad source" } 74 | guard let destination = storedOperations.metadata?.destinationContent else { return "bad destination"} 75 | return MultiLineDiff.generateASCIIDiff(source: source, destination: destination) 76 | } 77 | 78 | func getDiffResultFromHash(diffHash: String) throws -> DiffResult { 79 | // Try to retrieve diff operations from the dictionary 80 | guard let storedOperations = DiffOperationDict[diffHash] else { 81 | throw NSError(domain: "XcfSwiftDiff", code: 5, userInfo: [ 82 | NSLocalizedDescriptionKey: "No diff operations found for the given hash" 83 | ]) 84 | } 85 | return storedOperations 86 | } 87 | 88 | /// Applies a diff to a document 89 | /// - Parameters: 90 | /// - filePath: Path to the source document 91 | /// - operations: The diff operations to apply 92 | /// - Returns: True if successful, false otherwise 93 | /// - Throws: File access, parsing, or application errors 94 | func applyDiffToDocument( 95 | filePath: String, 96 | operations: DiffResult 97 | ) throws -> Bool { 98 | // Resolve the file path 99 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 100 | 101 | // Validate file exists 102 | guard FileManager.default.fileExists(atPath: resolvedPath) else { 103 | throw NSError(domain: "XcfSwiftDiff", code: 1, userInfo: [ 104 | NSLocalizedDescriptionKey: "File not found: \(filePath)" 105 | ]) 106 | } 107 | 108 | // Read the original content using ScriptingBridge 109 | guard let originalContent = XcfSwiftScript.shared.readSwiftDocumentWithFileManager(filePath: resolvedPath) else { 110 | throw NSError(domain: "XcfSwiftDiff", code: 3, userInfo: [ 111 | NSLocalizedDescriptionKey: "Failed to read document content" 112 | ]) 113 | } 114 | 115 | // Apply the diff to the entire content using the existing applyDiff function 116 | let modifiedContent = try applyDiff(original: originalContent, diff: operations) 117 | 118 | // Write the modified content back to the file using ScriptingBridge 119 | if !XcfSwiftScript.shared.writeSwiftDocumentWithFileManager(filePath: resolvedPath, content: modifiedContent) { 120 | throw NSError(domain: "XcfSwiftDiff", code: 4, userInfo: [ 121 | NSLocalizedDescriptionKey: "Failed to write modified content to document" 122 | ]) 123 | } 124 | 125 | return true 126 | } 127 | 128 | // Add new function to create diff from document and store in dictionary 129 | func createDiffFromString(original: String, modified: String) throws -> String { 130 | 131 | // Create the diff operations 132 | let diffResult = createDiff(original: original, modified: modified) 133 | 134 | guard let diffHash = diffResult.metadata?.diffHash else { 135 | return "The Diff did not create a hash" 136 | } 137 | 138 | // Store the diff operations in the dictionary 139 | DiffOperationDict[diffHash] = diffResult 140 | 141 | return diffHash 142 | } 143 | 144 | // Add new function to create diff from document and store in dictionary 145 | func applyDiffFromString(original: String, diffHash: String) throws -> String { 146 | 147 | // Store the diff operations in the dictionary 148 | guard let diff = DiffOperationDict[diffHash] else { 149 | return "Diff hash not found" 150 | } 151 | 152 | // Create the diff operations 153 | return try applyDiff(original: original, diff: diff) 154 | 155 | } 156 | 157 | // Add new function to create diff from document and store in dictionary 158 | func createDiffFromDocument(filePath: String, modifiedContent: String) throws -> String { 159 | // Resolve the file path 160 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 161 | 162 | // Validate file exists 163 | guard FileManager.default.fileExists(atPath: resolvedPath) else { 164 | throw NSError(domain: "XcfSwiftDiff", code: 1, userInfo: [ 165 | NSLocalizedDescriptionKey: "File not found: \(filePath)" 166 | ]) 167 | } 168 | 169 | // Read the original content using ScriptingBridge 170 | guard let originalContent = XcfSwiftScript.shared.readSwiftDocumentWithFileManager(filePath: resolvedPath) else { 171 | throw NSError(domain: "XcfSwiftDiff", code: 3, userInfo: [ 172 | NSLocalizedDescriptionKey: "Failed to read document content" 173 | ]) 174 | } 175 | 176 | // Create the diff operations 177 | let diffResult = createDiff(original: originalContent, modified: modifiedContent) 178 | 179 | // Create a new hash for this diff operation 180 | let diffHash = diffResult.metadata?.diffHash ?? "Diff hash is missing" 181 | 182 | // Store the diff operations in the dictionary 183 | DiffOperationDict[diffHash] = diffResult 184 | 185 | return diffHash 186 | } 187 | -------------------------------------------------------------------------------- /xcf/Mcp/Tools/XcfMcpFileSystemHandlers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - File System Tool Handlers 5 | 6 | class XcfMcpFileSystemHandlers { 7 | 8 | /// Handles a call to the read file tool 9 | static func handleReadFileToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 10 | guard let arguments = params.arguments else { 11 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 12 | } 13 | 14 | // Try to get filePath from arguments in two ways: 15 | // 1. As a named parameter (filePath=...) 16 | // 2. As a direct argument (first argument after command) 17 | let filePath: String 18 | if let namedPath = arguments[McpConfig.filePathParamName]?.stringValue { 19 | filePath = namedPath 20 | } else if let firstArg = arguments.first?.value.stringValue { 21 | filePath = firstArg 22 | } else { 23 | return CallTool.Result(content: [.text(McpConfig.missingFilePathParamError)]) 24 | } 25 | 26 | // Get and verify the project directory 27 | guard let projectDir = XcfXcodeProjectManager.shared.currentFolder else { 28 | return CallTool.Result(content: [.text("No current project directory set")]) 29 | } 30 | 31 | // Determine the full path 32 | let fullPath = FuzzyLogicService.expandPath(filePath, relativeTo: projectDir) 33 | 34 | do { 35 | let fileContents = try String(contentsOfFile: fullPath, encoding: .utf8) 36 | return CallTool.Result(content: [.text(fileContents)]) 37 | } catch { 38 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorReadingFile, error.localizedDescription))]) 39 | } 40 | } 41 | 42 | /// Handles a call to the change directory tool 43 | static func handleCdDirToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 44 | // Get the current folder first as we'll need it 45 | guard let currentFolder = XcfXcodeProjectManager.shared.currentFolder else { 46 | return CallTool.Result(content: [.text("No current folder is set. Please select a project first.")]) 47 | } 48 | 49 | // If no arguments provided, show current directory 50 | if params.arguments == nil || params.arguments?.isEmpty == true { 51 | return CallTool.Result(content: [.text("Current directory: \(currentFolder)")]) 52 | } 53 | 54 | let arguments = params.arguments! 55 | 56 | // Determine the directory to change to 57 | let directoryPath: String 58 | if let namedPath = arguments[McpConfig.directoryPathParamName]?.stringValue { 59 | // Case 1: Named parameter 60 | if namedPath == "." { 61 | directoryPath = currentFolder 62 | } else if namedPath == ".." { 63 | directoryPath = (currentFolder as NSString).deletingLastPathComponent 64 | } else { 65 | directoryPath = namedPath 66 | } 67 | } else if let firstArg = arguments.first?.value.stringValue { 68 | // Case 2: First argument 69 | if firstArg == "." { 70 | directoryPath = currentFolder 71 | } else if firstArg == ".." { 72 | directoryPath = (currentFolder as NSString).deletingLastPathComponent 73 | } else { 74 | directoryPath = firstArg 75 | } 76 | } else { 77 | // Default to showing current directory 78 | return CallTool.Result(content: [.text("Current directory: \(currentFolder)")]) 79 | } 80 | 81 | // Security check 82 | let (allowed, resolvedPath, error) = SecurityManager.shared.isDirectoryOperationAllowed(directoryPath, operation: "change to") 83 | if !allowed { 84 | return CallTool.Result(content: [.text(error ?? "Access denied")]) 85 | } 86 | 87 | do { 88 | // Update both FileManager's current directory and XcfXcodeProjectManager's currentFolder 89 | try XcfFileManager.changeDirectory(to: resolvedPath) 90 | XcfXcodeProjectManager.shared.currentFolder = resolvedPath 91 | return CallTool.Result(content: [.text("Changed directory to: \(resolvedPath)")]) 92 | } catch { 93 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorChangingDirectory, error.localizedDescription))]) 94 | } 95 | } 96 | 97 | /// Handles a call to the read directory tool 98 | static func handleReadDirToolCall(_ params: CallTool.Parameters) throws -> CallTool.Result { 99 | // Get the current folder first as we'll need it 100 | guard let currentFolder = XcfXcodeProjectManager.shared.currentFolder else { 101 | return CallTool.Result(content: [.text("No current folder is set. Please select a project first.")]) 102 | } 103 | 104 | let arguments = params.arguments! 105 | 106 | // Determine the directory to read 107 | let directoryPath: String 108 | if let namedPath = arguments[McpConfig.directoryPathParamName]?.stringValue { 109 | // Case 1: Named parameter 110 | if namedPath == "." || params.arguments == nil || params.arguments?.isEmpty == true { 111 | directoryPath = currentFolder 112 | } else if namedPath == ".." { 113 | directoryPath = (currentFolder as NSString).deletingLastPathComponent 114 | } else if namedPath == "cd" { 115 | do { 116 | let filePaths = try XcfFileManager.readDirectory(at: currentFolder, fileExtension: nil) 117 | let content = filePaths.joined(separator: McpConfig.newLineSeparator) 118 | return CallTool.Result(content: [.text(content)]) 119 | } catch { 120 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorReadingDirectory, error.localizedDescription))]) 121 | } 122 | } else { 123 | directoryPath = namedPath 124 | } 125 | } else if let firstArg = arguments.first?.value.stringValue { 126 | // Case 2: First argument 127 | if firstArg == "." { 128 | directoryPath = currentFolder 129 | } else if firstArg == ".." { 130 | directoryPath = (currentFolder as NSString).deletingLastPathComponent 131 | } else { 132 | directoryPath = firstArg 133 | } 134 | } else { 135 | // Default to current folder 136 | directoryPath = currentFolder 137 | } 138 | 139 | // Security check 140 | let (allowed, resolvedPath, error) = SecurityManager.shared.isDirectoryOperationAllowed(directoryPath, operation: "read") 141 | if !allowed { 142 | return CallTool.Result(content: [.text(error ?? "Access denied")]) 143 | } 144 | 145 | // Get optional file extension filter 146 | let fileExtension: String? 147 | if let namedExt = arguments[McpConfig.fileExtensionParamName]?.stringValue { 148 | fileExtension = namedExt.isEmpty ? nil : namedExt 149 | } else { 150 | let keys = arguments.keys.sorted() 151 | if keys.count >= 2 { 152 | fileExtension = arguments[keys[1]]?.stringValue 153 | } else { 154 | fileExtension = nil 155 | } 156 | } 157 | 158 | do { 159 | let filePaths = try XcfFileManager.readDirectory(at: resolvedPath, fileExtension: fileExtension) 160 | let content = filePaths.joined(separator: McpConfig.newLineSeparator) 161 | return CallTool.Result(content: [.text(content)]) 162 | } catch { 163 | return CallTool.Result(content: [.text(String(format: ErrorMessages.errorReadingDirectory, error.localizedDescription))]) 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /xcf-quickref.md: -------------------------------------------------------------------------------- 1 | # XCF Quick Reference Guide 🚀 2 | 3 | ## 🎛️ XCF Core Actions 4 | 5 | ### Permissions & Project Management 6 | | Action | Description | Example | Output | 7 | |--------|-------------|---------|--------| 8 | | `grant` | Grant Xcode automation | `grant` | Permissions granted | 9 | | `show` | List open projects | `show` | 1. /path/to/project.xcodeproj | 10 | | `open #` | Select project | `open 1` | Project selected | 11 | | `current` | Show selected project | `current` | /path/to/current/project | 12 | 13 | ### Build & Execute 14 | | Action | Description | Example | Output | 15 | |--------|-------------|---------|--------| 16 | | `build` | Build project | `build` | 🐦📜 Built successfully | 17 | | `run` | Run project | `run` | 🐦📜 Ran successfully | 18 | 19 | ### System & Analysis 20 | | Action | Description | Example | Output | 21 | |--------|-------------|---------|--------| 22 | | `env` | Show environment | `env` | [Environment variables] | 23 | | `pwd` | Show current folder | `pwd` | /current/directory | 24 | | `analyze ` | Analyze Swift code | `analyze main.swift` | Code analysis report | 25 | | `lz ` | Quick code analysis | `lz main.swift` | Condensed analysis | 26 | 27 | ### Aliases 28 | - `pwd` = `dir` = `path` 29 | - `lz` = shorthand for `analyze` 30 | 31 | ### Quick Workflow 32 | ```bash 33 | grant # Authorize XCF 34 | show # List projects 35 | open 1 # Select project 36 | build # Compile project 37 | run # Execute project 38 | ``` 39 | 40 | ### Code Analysis Workflow 41 | ```bash 42 | analyze main.swift # Detailed analysis 43 | lz main.swift # Quick analysis 44 | ``` 45 | 46 | ## 📋 Core Commands 47 | 48 | ### Activation & Project Management 49 | | Command | Description | Example | Output | 50 | |---------|-------------|---------|--------| 51 | | `show` | List open Xcode projects | `show` | 1. /path/to/project.xcodeproj | 52 | | `open #` | Select project by number | `open 1` | [Selects project] | 53 | | `current` | Show current project | `current` | Current project: /path/to/project.xcodeproj | 54 | 55 | ### Build & Run 56 | | Command | Description | Example | Output | 57 | |---------|-------------|---------|--------| 58 | | `build` | Build current project | `build` | 🐦📜 Built successfully | 59 | | `run` | Run current project | `run` | 🐦📜 Ran successfully | 60 | 61 | ### System Information 62 | | Command | Description | Example | Output | 63 | |---------|-------------|---------|--------| 64 | | `env` | Show environment variables | `env` | [List of environment variables] | 65 | | `pwd` | Show current folder | `pwd` | Current folder: /path/to/folder | 66 | | `help` | Display all commands | `help` | [List of available commands] | 67 | 68 | ## 🔍 Path Resolution & Quoting 69 | 70 | ### Supported Path Types 71 | - Current directory: `file.swift` 72 | - Child directories: `src/file.swift` 73 | - Parent directory: `../file.swift` 74 | - Multiple directories up: `../../file.swift` 75 | - Full system paths: `/Users/username/project/file.swift` 76 | 77 | ### Quoting Rules 78 | - Use quotes for content with spaces 79 | - Single `'` or double `"` quotes work 80 | - Recommended for complex content 81 | 82 | ### Path Resolution Examples 83 | ```bash 84 | read_file main.swift # Current directory 85 | read_file src/utils.swift # Child directory 86 | read_file ../shared/config.swift # Parent directory 87 | write_file test.txt "Hello World" # Simple content 88 | write_file config.json '{"key": "value"}' # JSON content 89 | ``` 90 | 91 | ## 🗂️ File Operations 92 | | Command | Description | Example | 93 | |---------|-------------|---------| 94 | | `read_file ` | Read file contents | `read_file main.swift` | 95 | | `write_file ` | Write to file | `write_file test.txt "Hello World"` | 96 | | `edit_file ` | Edit file lines | `edit_file main.swift 10 20 "Updated code"` | 97 | | `delete_file ` | Delete file | `delete_file temp.txt` | 98 | | `move_file ` | Move file | `move_file old.swift new.swift` | 99 | 100 | ## 📂 Directory Operations 101 | | Command | Description | Example | 102 | |---------|-------------|---------| 103 | | `cd_dir ` | Change directory | `cd_dir /path/to/project` | 104 | | `read_dir [path] [ext]` | List directory contents | `read_dir . swift` | 105 | | `add_dir ` | Create directory | `add_dir new_folder` | 106 | | `rm_dir ` | Remove directory | `rm_dir old_folder` | 107 | | `move_dir ` | Move directory | `move_dir old_dir new_dir` | 108 | 109 | ## 📄 Xcode Document Operations 110 | | Command | Description | Example | 111 | |---------|-------------|---------| 112 | | `open_doc ` | Open in Xcode | `open_doc main.swift` | 113 | | `create_doc [content]` | Create new document | `create_doc new.swift "import Foundation"` | 114 | | `read_doc ` | Read Xcode document | `read_doc main.swift` | 115 | | `save_doc ` | Save Xcode document | `save_doc main.swift` | 116 | 117 | | `close_doc ` | Close document | `close_doc main.swift true` | 118 | 119 | ## 🔍 Code Analysis 120 | | Command | Description | Example | 121 | |---------|-------------|---------| 122 | | `snippet [start] [end]` | Extract code snippets | `snippet main.swift 10 20` | 123 | | `analyzer [start] [end]` | Analyze Swift code | `analyzer main.swift 10 50` | 124 | | `lz ` | Shorthand analyzer | `lz main.swift` | 125 | 126 | ## 🤖 AI Assistant MCP Tools 127 | | Tool | Purpose | Example | 128 | |------|---------|---------| 129 | | `mcp_xcf_xcf` | Execute XCF actions | `mcp_xcf_xcf(action="build")` | 130 | | `mcp_xcf_snippet` | Extract code snippets | `mcp_xcf_snippet(filePath="main.swift", entireFile=true)` | 131 | | `mcp_xcf_analyzer` | Analyze Swift code | `mcp_xcf_analyzer(filePath="main.swift", entireFile=true)` | 132 | | `mcp_xcf_read_file` | Read file contents | `mcp_xcf_read_file(filePath="main.swift")` | 133 | | `mcp_xcf_write_file` | Write file contents | `mcp_xcf_write_file(filePath="test.txt", content="Hello World")` | 134 | 135 | ## 🎮 Standalone Action Tools 136 | | Tool | Purpose | Example | 137 | |------|---------|---------| 138 | | `mcp_xcf_show_help` | Display help | `mcp_xcf_show_help()` | 139 | | `mcp_xcf_grant_permission` | Grant permissions | `mcp_xcf_grant_permission()` | 140 | | `mcp_xcf_run_project` | Run project | `mcp_xcf_run_project()` | 141 | | `mcp_xcf_build_project` | Build project | `mcp_xcf_build_project()` | 142 | | `mcp_xcf_show_current_project` | Show project | `mcp_xcf_show_current_project()` | 143 | | `mcp_xcf_show_env` | Show env vars | `mcp_xcf_show_env()` | 144 | | `mcp_xcf_show_folder` | Show directory | `mcp_xcf_show_folder()` | 145 | | `mcp_xcf_list_projects` | List projects | `mcp_xcf_list_projects()` | 146 | | `mcp_xcf_select_project` | Select project | `mcp_xcf_select_project(projectNumber=1)` | 147 | | `mcp_xcf_analyze_swift_code` | Analyze code | `mcp_xcf_analyze_swift_code(filePath="main.swift")` | 148 | 149 | ## 🚀 Quick Workflow Examples 150 | 151 | ### Basic Project Workflow 152 | ``` 153 | use_xcf 154 | xcf pwd # Check current directory 155 | cd_dir . # Confirm directory 156 | show 157 | open 1 158 | build 159 | run 160 | ``` 161 | 162 | ### Code Analysis Workflow 163 | ``` 164 | use_xcf 165 | xcf pwd # Verify working directory 166 | cd_dir . # Ensure correct context 167 | current 168 | snippet main.swift 169 | lz main.swift 170 | build 171 | ``` 172 | 173 | ### File Manipulation Workflow 174 | ``` 175 | use_xcf 176 | xcf pwd # Check current directory 177 | cd_dir . # Confirm directory 178 | read_dir . 179 | read_file main.swift 180 | edit_file main.swift 10 15 "// Updated code" 181 | build 182 | ``` 183 | 184 | ## 🔒 Security & Performance Tips 185 | - Always activate XCF before operations 186 | - Use specific file paths 187 | - Leverage smart path resolution 188 | - Implement error handling 189 | - Use code analysis before major changes 190 | 191 | ## 🔍 Directory Management Tips 192 | - Always use `xcf pwd` before operations 193 | - Use `cd_dir .` to confirm current directory 194 | - Verify path before file/directory actions 195 | 196 | --- 197 | 198 | Created by XCodeFreeze Automation - Swift Development at Light Speed! 🚀 -------------------------------------------------------------------------------- /.cursor/rules/xcf-quickref.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: xcf Xcode MCP Server - Quick Reference 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # XCF Quick Reference Guide 🚀 7 | 8 | ## 🎛️ XCF Core Actions 9 | 10 | ### Permissions & Project Management 11 | | Action | Description | Example | Output | 12 | |--------|-------------|---------|--------| 13 | | `grant` | Grant Xcode automation | `grant` | Permissions granted | 14 | | `show` | List open projects | `show` | 1. /path/to/project.xcodeproj | 15 | | `open #` | Select project | `open 1` | Project selected | 16 | | `current` | Show selected project | `current` | /path/to/current/project | 17 | 18 | ### Build & Execute 19 | | Action | Description | Example | Output | 20 | |--------|-------------|---------|--------| 21 | | `build` | Build project | `build` | 🐦📜 Built successfully | 22 | | `run` | Run project | `run` | 🐦📜 Ran successfully | 23 | 24 | ### System & Analysis 25 | | Action | Description | Example | Output | 26 | |--------|-------------|---------|--------| 27 | | `env` | Show environment | `env` | [Environment variables] | 28 | | `pwd` | Show current folder | `pwd` | /current/directory | 29 | | `analyze ` | Analyze Swift code | `analyze main.swift` | Code analysis report | 30 | | `lz ` | Quick code analysis | `lz main.swift` | Condensed analysis | 31 | 32 | ### Aliases 33 | - `pwd` = `dir` = `path` 34 | - `lz` = shorthand for `analyze` 35 | 36 | ### Quick Workflow 37 | ```bash 38 | grant # Authorize XCF 39 | show # List projects 40 | open 1 # Select project 41 | build # Compile project 42 | run # Execute project 43 | ``` 44 | 45 | ### Code Analysis Workflow 46 | ```bash 47 | analyze main.swift # Detailed analysis 48 | lz main.swift # Quick analysis 49 | ``` 50 | 51 | ## 📋 Core Commands 52 | 53 | ### Activation & Project Management 54 | | Command | Description | Example | Output | 55 | |---------|-------------|---------|--------| 56 | | `show` | List open Xcode projects | `show` | 1. /path/to/project.xcodeproj | 57 | | `open #` | Select project by number | `open 1` | [Selects project] | 58 | | `current` | Show current project | `current` | Current project: /path/to/project.xcodeproj | 59 | 60 | ### Build & Run 61 | | Command | Description | Example | Output | 62 | |---------|-------------|---------|--------| 63 | | `build` | Build current project | `build` | 🐦📜 Built successfully | 64 | | `run` | Run current project | `run` | 🐦📜 Ran successfully | 65 | 66 | ### System Information 67 | | Command | Description | Example | Output | 68 | |---------|-------------|---------|--------| 69 | | `env` | Show environment variables | `env` | [List of environment variables] | 70 | | `pwd` | Show current folder | `pwd` | Current folder: /path/to/folder | 71 | | `help` | Display all commands | `help` | [List of available commands] | 72 | 73 | ## 🔍 Path Resolution & Quoting 74 | 75 | ### Supported Path Types 76 | - Current directory: `file.swift` 77 | - Child directories: `src/file.swift` 78 | - Parent directory: `../file.swift` 79 | - Multiple directories up: `../../file.swift` 80 | - Full system paths: `/Users/username/project/file.swift` 81 | 82 | ### Quoting Rules 83 | - Use quotes for content with spaces 84 | - Single `'` or double `"` quotes work 85 | - Recommended for complex content 86 | 87 | ### Path Resolution Examples 88 | ```bash 89 | read_file main.swift # Current directory 90 | read_file src/utils.swift # Child directory 91 | read_file ../shared/config.swift # Parent directory 92 | write_file test.txt "Hello World" # Simple content 93 | write_file config.json '{"key": "value"}' # JSON content 94 | ``` 95 | 96 | ## 🗂️ File Operations 97 | | Command | Description | Example | 98 | |---------|-------------|---------| 99 | | `read_file ` | Read file contents | `read_file main.swift` | 100 | | `write_file ` | Write to file | `write_file test.txt "Hello World"` | 101 | | `edit_file ` | Edit file lines | `edit_file main.swift 10 20 "Updated code"` | 102 | | `delete_file ` | Delete file | `delete_file temp.txt` | 103 | | `move_file ` | Move file | `move_file old.swift new.swift` | 104 | 105 | ## 📂 Directory Operations 106 | | Command | Description | Example | 107 | |---------|-------------|---------| 108 | | `cd_dir ` | Change directory | `cd_dir /path/to/project` | 109 | | `read_dir [path] [ext]` | List directory contents | `read_dir . swift` | 110 | | `add_dir ` | Create directory | `add_dir new_folder` | 111 | | `rm_dir ` | Remove directory | `rm_dir old_folder` | 112 | | `move_dir ` | Move directory | `move_dir old_dir new_dir` | 113 | 114 | ## 📄 Xcode Document Operations 115 | | Command | Description | Example | 116 | |---------|-------------|---------| 117 | | `open_doc ` | Open in Xcode | `open_doc main.swift` | 118 | | `create_doc [content]` | Create new document | `create_doc new.swift "import Foundation"` | 119 | | `read_doc ` | Read Xcode document | `read_doc main.swift` | 120 | | `save_doc ` | Save Xcode document | `save_doc main.swift` | 121 | 122 | | `close_doc ` | Close document | `close_doc main.swift true` | 123 | 124 | ## 🔍 Code Analysis 125 | | Command | Description | Example | 126 | |---------|-------------|---------| 127 | | `snippet [start] [end]` | Extract code snippets | `snippet main.swift 10 20` | 128 | | `analyzer [start] [end]` | Analyze Swift code | `analyzer main.swift 10 50` | 129 | | `lz ` | Shorthand analyzer | `lz main.swift` | 130 | 131 | ## 🤖 AI Assistant MCP Tools 132 | | Tool | Purpose | Example | 133 | |------|---------|---------| 134 | | `mcp_xcf_xcf` | Execute XCF actions | `mcp_xcf_xcf(action="build")` | 135 | | `mcp_xcf_snippet` | Extract code snippets | `mcp_xcf_snippet(filePath="main.swift", entireFile=true)` | 136 | | `mcp_xcf_analyzer` | Analyze Swift code | `mcp_xcf_analyzer(filePath="main.swift", entireFile=true)` | 137 | | `mcp_xcf_read_file` | Read file contents | `mcp_xcf_read_file(filePath="main.swift")` | 138 | | `mcp_xcf_write_file` | Write file contents | `mcp_xcf_write_file(filePath="test.txt", content="Hello World")` | 139 | 140 | ## 🎮 Standalone Action Tools 141 | | Tool | Purpose | Example | 142 | |------|---------|---------| 143 | | `mcp_xcf_show_help` | Display help | `mcp_xcf_show_help()` | 144 | | `mcp_xcf_grant_permission` | Grant permissions | `mcp_xcf_grant_permission()` | 145 | | `mcp_xcf_run_project` | Run project | `mcp_xcf_run_project()` | 146 | | `mcp_xcf_build_project` | Build project | `mcp_xcf_build_project()` | 147 | | `mcp_xcf_show_current_project` | Show project | `mcp_xcf_show_current_project()` | 148 | | `mcp_xcf_show_env` | Show env vars | `mcp_xcf_show_env()` | 149 | | `mcp_xcf_show_folder` | Show directory | `mcp_xcf_show_folder()` | 150 | | `mcp_xcf_list_projects` | List projects | `mcp_xcf_list_projects()` | 151 | | `mcp_xcf_select_project` | Select project | `mcp_xcf_select_project(projectNumber=1)` | 152 | | `mcp_xcf_analyze_swift_code` | Analyze code | `mcp_xcf_analyze_swift_code(filePath="main.swift")` | 153 | 154 | ## 🚀 Quick Workflow Examples 155 | 156 | ### Basic Project Workflow 157 | ``` 158 | use_xcf 159 | xcf pwd # Check current directory 160 | cd_dir . # Confirm directory 161 | show 162 | open 1 163 | build 164 | run 165 | ``` 166 | 167 | ### Code Analysis Workflow 168 | ``` 169 | use_xcf 170 | cd_dir . # Ensure correct context 171 | current 172 | snippet main.swift 173 | lz main.swift 174 | build 175 | ``` 176 | 177 | ### File Manipulation Workflow 178 | ``` 179 | use_xcf 180 | xcf pwd # Check current directory 181 | read_dir . 182 | read_file main.swift 183 | edit_file main.swift 10 15 "// Updated code" 184 | build 185 | ``` 186 | 187 | ## 🔒 Security & Performance Tips 188 | - Always activate XCF before operations 189 | - Use specific file paths 190 | - Leverage smart path resolution 191 | - Implement error handling 192 | - Use code analysis before major changes 193 | 194 | ## 🔍 Directory Management Tips 195 | - Always use `xcf pwd` before operations 196 | - OR 'cd_dir .` to confirm current directory 197 | - Verify path before file/directory actions 198 | 199 | --- 200 | 201 | Created by XCodeFreeze Automation - Swift Development at Light Speed! 🚀 202 | -------------------------------------------------------------------------------- /xcf-ai-guide.md: -------------------------------------------------------------------------------- 1 | # XCF - AI Assistant Integration Guide 2 | 3 | ## 🤖 Overview for AI Assistants 4 | 5 | XCF (Xcode MCP Server) is a powerful Swift-based automation tool designed to streamline Xcode project management for AI assistants. This guide provides comprehensive instructions for integrating XCF into AI-powered development workflows. 6 | 7 | ## 🔧 Configuration 8 | 9 | ### MCP Server Setup 10 | 11 | Add XCF to your MCP configuration file with the following JSON: 12 | 13 | ```json 14 | { 15 | "mcpServers": { 16 | "xcf": { 17 | "type": "stdio", 18 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server" 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | #### Configuration Locations 25 | - **Cursor**: `~/.cursor/mcp.json` 26 | - **Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` 27 | 28 | ### Advanced Project Configuration 29 | 30 | For project-specific control, use environment variables: 31 | 32 | ```json 33 | { 34 | "mcpServers": { 35 | "xcf": { 36 | "type": "stdio", 37 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server", 38 | "env": { 39 | "XCODE_PROJECT_FOLDER": "/path/to/project/", 40 | "XCODE_PROJECT": "/path/to/project/project.xcodeproj" 41 | } 42 | } 43 | } 44 | } 45 | ``` 46 | 47 | ## 🛠️ AI-Powered Workflow Tools 48 | 49 | ### MCP Function Tools 50 | 51 | XCF provides a rich set of tools for AI assistants: 52 | 53 | | Tool | Purpose | Example Usage | 54 | |------|---------|---------------| 55 | | `mcp_xcf_xcf` | Execute XCF actions | `mcp_xcf_xcf(action="build")` | 56 | | `mcp_xcf_snippet` | Extract code snippets | `mcp_xcf_snippet(filePath="main.swift", entireFile=true)` | 57 | | `mcp_xcf_analyzer` | Analyze Swift code | `mcp_xcf_analyzer(filePath="main.swift", entireFile=true)` | 58 | | `mcp_xcf_read_file` | Read file contents | `mcp_xcf_read_file(filePath="main.swift")` | 59 | | `mcp_xcf_write_file` | Write file contents | `mcp_xcf_write_file(filePath="test.txt", content="Hello World")` | 60 | 61 | ### Standalone Action Tools 62 | 63 | In addition to the general-purpose tools, XCF now provides dedicated tools for each action: 64 | 65 | | Tool | Purpose | Example Usage | 66 | |------|---------|---------------| 67 | | `mcp_xcf_show_help` | Display help information | `mcp_xcf_show_help()` | 68 | | `mcp_xcf_grant_permission` | Grant Xcode permissions | `mcp_xcf_grant_permission()` | 69 | | `mcp_xcf_run_project` | Run the current project | `mcp_xcf_run_project()` | 70 | | `mcp_xcf_build_project` | Build the current project | `mcp_xcf_build_project()` | 71 | | `mcp_xcf_show_current_project` | Show selected project | `mcp_xcf_show_current_project()` | 72 | | `mcp_xcf_show_env` | Display environment variables | `mcp_xcf_show_env()` | 73 | | `mcp_xcf_show_folder` | Show current directory | `mcp_xcf_show_folder()` | 74 | | `mcp_xcf_list_projects` | List all Xcode projects | `mcp_xcf_list_projects()` | 75 | | `mcp_xcf_select_project` | Select a project by number | `mcp_xcf_select_project(projectNumber=1)` | 76 | | `mcp_xcf_analyze_swift_code` | Analyze Swift code | `mcp_xcf_analyze_swift_code(filePath="main.swift")` | 77 | 78 | ### Workflow Patterns 79 | 80 | #### Basic Project Management 81 | 82 | ``` 83 | # Activate XCF 84 | use_xcf 85 | 86 | # List and select project 87 | show 88 | open 1 89 | 90 | # Build and run 91 | build 92 | run 93 | ``` 94 | 95 | #### Code Analysis Workflow 96 | 97 | ``` 98 | # Activate XCF 99 | use_xcf 100 | 101 | # Get current project 102 | current 103 | 104 | # Extract and analyze code 105 | snippet main.swift 106 | analyzer main.swift 107 | 108 | # Implement fixes 109 | edit_file main.swift 10 20 "# Improved implementation" 110 | 111 | # Rebuild 112 | build 113 | ``` 114 | 115 | ## 🔍 Smart Path Resolution 116 | 117 | XCF uses intelligent path resolution for file operations: 118 | 119 | 1. Exact path provided 120 | 2. Relative path from current directory 121 | 3. Current project directory 122 | 4. Workspace folder 123 | 5. Recursive workspace search 124 | 6. Fuzzy filename matching 125 | 126 | ### File Access Examples 127 | 128 | ```python 129 | # These are equivalent 130 | mcp_xcf_snippet(filePath="/full/path/to/file.swift") 131 | mcp_xcf_snippet(filePath="file.swift") # Smart resolution 132 | ``` 133 | 134 | ## 📊 Code Analysis Capabilities 135 | 136 | The Swift code analyzer provides: 137 | - Code style checks 138 | - Complexity analysis 139 | - Unused variable detection 140 | - Magic number identification 141 | - Refactoring suggestions 142 | - Method length evaluation 143 | 144 | ### Analysis Example 145 | 146 | ```python 147 | # Analyze entire file 148 | mcp_xcf_analyzer(filePath="main.swift", entireFile=true) 149 | 150 | # Analyze specific line range 151 | mcp_xcf_analyzer( 152 | filePath="main.swift", 153 | startLine=10, 154 | endLine=50 155 | ) 156 | ``` 157 | 158 | ## 🔒 Security Considerations 159 | 160 | - Workspace-bound operations 161 | - Automatic access prevention outside workspace 162 | - Environment variable-based security 163 | - Safe redirection of potentially unsafe actions 164 | 165 | ## 🚀 Performance Tips 166 | 167 | - Use specific file paths when possible 168 | - Leverage smart path resolution 169 | - Utilize line-range analysis for large files 170 | - Prefer MCP function tools for programmatic interactions 171 | 172 | ## 🤝 Integration Best Practices 173 | 174 | 1. Always activate XCF before operations 175 | 2. Use environment variables for project configuration 176 | 3. Leverage smart path resolution 177 | 4. Implement error handling 178 | 5. Use code analysis before major changes 179 | 180 | ## 📋 Comprehensive Tool List 181 | 182 | Refer to the User Guide for a complete list of available tools and their detailed usage. 183 | 184 | ## 🎛️ XCF Core Actions 185 | 186 | ### Available Actions 187 | 188 | | Action | Description | Example | Purpose | 189 | |--------|-------------|---------|---------| 190 | | `grant` | Grant Xcode automation permissions | `mcp_xcf_xcf(action="grant")` | Authorize XCF to interact with Xcode | 191 | | `show` | List open projects | `mcp_xcf_xcf(action="show")` | Display available Xcode projects | 192 | | `open #` | Select project by number | `mcp_xcf_xcf(action="open 1")` | Choose a specific project to work on | 193 | | `current` | Show selected project | `mcp_xcf_xcf(action="current")` | Display the currently active project | 194 | | `build` | Build current project | `mcp_xcf_xcf(action="build")` | Compile the selected Xcode project | 195 | | `run` | Run current project | `mcp_xcf_xcf(action="run")` | Execute the current project | 196 | | `env` | Show environment variables | `mcp_xcf_xcf(action="env")` | Display system environment configuration | 197 | | `pwd` | Show current folder | `mcp_xcf_xcf(action="pwd")` | Display current working directory | 198 | | `analyze` | Analyze Swift code | `mcp_xcf_xcf(action="analyze main.swift")` | Perform code analysis on a file | 199 | | `lz` | Shorthand for analyze | `mcp_xcf_xcf(action="lz main.swift")` | Quick code analysis | 200 | 201 | ### Comprehensive Workflow Example 202 | 203 | ``` 204 | # Full project lifecycle management 205 | mcp_xcf_xcf(action="grant") # Authorize XCF 206 | mcp_xcf_xcf(action="show") # List projects 207 | mcp_xcf_xcf(action="open 1") # Select first project 208 | mcp_xcf_xcf(action="current") # Confirm project 209 | mcp_xcf_xcf(action="build") # Build project 210 | mcp_xcf_xcf(action="run") # Run project 211 | 212 | # Code analysis workflow 213 | mcp_xcf_xcf(action="analyze main.swift") # Detailed analysis 214 | mcp_xcf_xcf(action="lz main.swift") # Quick analysis 215 | ``` 216 | 217 | ### Environment and Context Management 218 | 219 | ```python 220 | # Check environment and context 221 | mcp_xcf_xcf(action="env") # Show environment variables 222 | mcp_xcf_xcf(action="pwd") # Show current working directory 223 | ``` 224 | 225 | ### Aliases and Shortcuts 226 | 227 | Some commands have convenient aliases: 228 | - `pwd` can also use `dir` or `path` 229 | - `lz` is a quick shorthand for `analyze` 230 | 231 | ## 🔍 Smart Action Resolution 232 | 233 | XCF intelligently handles actions by: 234 | - Automatically selecting the most recent project 235 | - Providing fallback mechanisms 236 | - Offering clear, actionable feedback 237 | - Supporting relative and absolute paths 238 | 239 | --- 240 | 241 | **Pro Tip:** Always use `grant` first to ensure full XCF functionality! 242 | 243 | --- 244 | 245 | Created by XCodeFreeze Automation - Empowering AI-driven Swift development! -------------------------------------------------------------------------------- /xcf-user-guide.md: -------------------------------------------------------------------------------- 1 | # XCF Xcode MCP Server - Comprehensive User Guide 🚀 2 | 3 | ## 🌟 Introduction 4 | 5 | XCF (Xcode MCP Server) is a powerful, Swift-native automation tool designed to revolutionize your Xcode development workflow. This guide provides an in-depth look at installation, configuration, and usage of XCF. 6 | 7 | ## 🔧 Installation & Setup 8 | 9 | ### System Requirements 10 | - macOS (latest version recommended) 11 | - Xcode installed 12 | - Basic understanding of terminal and Swift development 13 | 14 | ### Installation Steps 15 | 16 | 1. **Download XCF** 17 | - Visit the official website or GitHub repository 18 | - Download the latest XCF application 19 | 20 | 2. **Install Application** 21 | - Drag the XCF.app to your `/Applications` folder 22 | - Launch the application once to approve internet downloads 23 | 24 | 3. **Codesign (if needed)** 25 | ```bash 26 | codesign --force --deep --sign - /Applications/xcf.app 27 | ``` 28 | 29 | ### Configuration 30 | 31 | #### MCP Server Configuration 32 | 33 | Add XCF to your MCP configuration file: 34 | 35 | ```json 36 | { 37 | "mcpServers": { 38 | "xcf": { 39 | "type": "stdio", 40 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server" 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | ##### Configuration Locations 47 | - **Cursor**: `~/.cursor/mcp.json` 48 | - **Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` 49 | 50 | #### Advanced Configuration 51 | 52 | For project-specific control: 53 | 54 | ```json 55 | { 56 | "mcpServers": { 57 | "xcf": { 58 | "type": "stdio", 59 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server", 60 | "env": { 61 | "XCODE_PROJECT_FOLDER": "/path/to/project/", 62 | "XCODE_PROJECT": "/path/to/project/project.xcodeproj" 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ## 🎬 Getting Started 70 | 71 | ### Activation 72 | 73 | Activate XCF mode: 74 | ``` 75 | use_xcf 76 | ``` 77 | 78 | ### Basic Workflow 79 | 80 | 1. List available projects 81 | ``` 82 | show 83 | ``` 84 | 85 | 2. Select a project 86 | ``` 87 | open 1 # Opens the first project in the list 88 | ``` 89 | 90 | 3. Build the project 91 | ``` 92 | build 93 | ``` 94 | 95 | 4. Run the project 96 | ``` 97 | run 98 | ``` 99 | 100 | ## 📂 File Operations 101 | 102 | ### Path Resolution and Quoting 103 | 104 | XCF supports flexible path resolution: 105 | - Current directory: `file.swift` 106 | - Child directories: `src/file.swift` 107 | - Parent directory: `../file.swift` 108 | - Multiple directories up: `../../file.swift` 109 | - Full system paths: `/Users/username/project/file.swift` 110 | 111 | **Quoting Rules:** 112 | - Use quotes for content with spaces 113 | - Can use single `'` or double `"` quotes 114 | - Recommended for complex content or paths with spaces 115 | 116 | ### Reading Files 117 | ```bash 118 | read_file main.swift # Current directory 119 | read_file src/utils.swift # Child directory 120 | read_file ../shared/config.swift # Parent directory 121 | read_file /full/path/to/main.swift # Full path 122 | ``` 123 | 124 | ### Writing Files 125 | ```bash 126 | write_file test.txt "Hello World" # Simple content 127 | write_file config.json '{"key": "value"}' # JSON content 128 | write_file "../shared/types.swift" 'import Foundation' # Quoted path 129 | ``` 130 | 131 | ### Editing Files 132 | ```bash 133 | edit_file main.swift 10 20 "Updated code" # Replace lines 10-20 134 | edit_file src/test.swift 5 5 "import UIKit" # Replace single line 135 | ``` 136 | 137 | ### File Management 138 | ```bash 139 | delete_file temp.txt # Delete file 140 | move_file old.swift new.swift # Move/rename file 141 | ``` 142 | 143 | ## 📁 Directory Operations 144 | 145 | ### Checking Current Directory 146 | ``` 147 | xcf pwd # Show current working directory 148 | cd_dir . # Confirm or reset to current directory 149 | ``` 150 | 151 | ### Changing Directories 152 | ``` 153 | cd_dir /path/to/project # Change to specific directory 154 | ``` 155 | 156 | ### Listing Contents 157 | ``` 158 | read_dir . # Current directory 159 | read_dir . swift # Swift files only 160 | ``` 161 | 162 | ### Directory Management 163 | - Create: `add_dir new_folder` 164 | - Remove: `rm_dir old_folder` 165 | - Move: `move_dir source_dir destination_dir` 166 | 167 | ### Best Practices 168 | 1. Always check current directory before file operations 169 | 2. Use `xcf pwd` to verify your working context 170 | 3. Use `cd_dir .` to ensure you're in the expected directory 171 | 172 | ## 📄 Xcode Document Management 173 | 174 | ### Opening Documents 175 | ```bash 176 | open_doc main.swift 177 | ``` 178 | 179 | ### Creating Documents 180 | ```bash 181 | create_doc new.swift "import Foundation" 182 | ``` 183 | 184 | ### Editing Documents 185 | ```bash 186 | 187 | ``` 188 | 189 | ### Document Lifecycle 190 | - Read: `read_doc main.swift` 191 | - Save: `save_doc main.swift` 192 | - Close: `close_doc main.swift true # With saving` 193 | 194 | ## 🔍 Code Analysis 195 | 196 | ### Snippet Extraction 197 | ```bash 198 | snippet main.swift # Entire file 199 | snippet main.swift 10 20 # Specific lines 200 | ``` 201 | 202 | ### Code Analysis 203 | ```bash 204 | analyzer main.swift # Full file analysis 205 | lz main.swift # Shorthand analysis 206 | ``` 207 | 208 | ### Analysis Features 209 | - Code style checks 210 | - Complexity evaluation 211 | - Unused variable detection 212 | - Refactoring suggestions 213 | - Method length analysis 214 | 215 | ## 🤖 AI Assistant Integration 216 | 217 | ### MCP Tools 218 | - `mcp_xcf_xcf`: Execute actions 219 | - `mcp_xcf_snippet`: Extract code 220 | - `mcp_xcf_analyzer`: Code analysis 221 | - `mcp_xcf_read_file`: Read files 222 | - `mcp_xcf_write_file`: Write files 223 | 224 | ### Standalone Action Tools 225 | XCF now provides direct tools for each action, making it easier to use and discover functionality: 226 | 227 | | Tool | Description | 228 | |------|-------------| 229 | | `mcp_xcf_show_help` | Display help information about available commands | 230 | | `mcp_xcf_grant_permission` | Grant Xcode automation permissions | 231 | | `mcp_xcf_run_project` | Run the current Xcode project | 232 | | `mcp_xcf_build_project` | Build the current Xcode project | 233 | | `mcp_xcf_show_current_project` | Show information about the currently selected project | 234 | | `mcp_xcf_show_env` | Display all environment variables | 235 | | `mcp_xcf_show_folder` | Display the current working folder | 236 | | `mcp_xcf_list_projects` | List all open Xcode projects | 237 | | `mcp_xcf_select_project` | Select an Xcode project by number | 238 | | `mcp_xcf_analyze_swift_code` | Analyze Swift code for potential issues | 239 | 240 | ### AI-Optimized Workflow Example 241 | ```python 242 | # Basic workflow using standalone tools 243 | mcp_xcf_use_xcf() # Activate XCF 244 | mcp_xcf_list_projects() # List available projects 245 | mcp_xcf_select_project(projectNumber=1) # Select first project 246 | mcp_xcf_build_project() # Build the project 247 | mcp_xcf_run_project() # Run the project 248 | 249 | # Code analysis using dedicated tools 250 | mcp_xcf_show_current_project() # Check current project 251 | mcp_xcf_analyze_swift_code(filePath="main.swift") # Analyze code 252 | ``` 253 | 254 | ## 🔒 Security & Best Practices 255 | 256 | ### Security Features 257 | - Workspace-bound operations 258 | - Automatic access prevention 259 | - Environment variable security 260 | - Safe action redirection 261 | 262 | ### Best Practices 263 | 1. Always activate XCF before operations 264 | 2. Use specific file paths 265 | 3. Leverage smart path resolution 266 | 4. Implement error handling 267 | 5. Analyze code before major changes 268 | 269 | ## 🚨 Troubleshooting 270 | 271 | ### Common Issues 272 | - Verify Xcode installation 273 | - Check XCF permissions 274 | - Validate configuration 275 | - Review environment variables 276 | 277 | ### Debugging Commands 278 | ```bash 279 | env # Show environment 280 | pwd # Current directory 281 | help # Available commands 282 | ``` 283 | 284 | ## 🌐 Community & Support 285 | 286 | - GitHub Repository: [XCF GitHub](https://github.com/codefreezeai/xcf) 287 | - Website: [xcf.ai](https://xcf.ai) 288 | - Community: Open to contributions! 289 | 290 | ## 📋 Version Information 291 | - Check current version with `xcf version` 292 | - Stay updated with latest releases 293 | 294 | --- 295 | 296 | Created by XCodeFreeze Automation - Empowering Swift Developers Worldwide! 🚀 -------------------------------------------------------------------------------- /.cursor/rules/xcf-ai-guide.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: xcf Xcode MCP Server - AI Guide 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # XCF - AI Assistant Integration Guide 7 | 8 | ## 🤖 Overview for AI Assistants 9 | 10 | XCF (Xcode MCP Server) is a powerful Swift-based automation tool designed to streamline Xcode project management for AI assistants. This guide provides comprehensive instructions for integrating XCF into AI-powered development workflows. 11 | 12 | ## 🔧 Configuration 13 | 14 | ### MCP Server Setup 15 | 16 | Add XCF to your MCP configuration file with the following JSON: 17 | 18 | ```json 19 | { 20 | "mcpServers": { 21 | "xcf": { 22 | "type": "stdio", 23 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server" 24 | } 25 | } 26 | } 27 | ``` 28 | 29 | #### Configuration Locations 30 | - **Cursor**: `~/.cursor/mcp.json` 31 | - **Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` 32 | 33 | ### Advanced Project Configuration 34 | 35 | For project-specific control, use environment variables: 36 | 37 | ```json 38 | { 39 | "mcpServers": { 40 | "xcf": { 41 | "type": "stdio", 42 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server", 43 | "env": { 44 | "XCODE_PROJECT_FOLDER": "/path/to/project/", 45 | "XCODE_PROJECT": "/path/to/project/project.xcodeproj" 46 | } 47 | } 48 | } 49 | } 50 | ``` 51 | 52 | ## 🛠️ AI-Powered Workflow Tools 53 | 54 | ### MCP Function Tools 55 | 56 | XCF provides a rich set of tools for AI assistants: 57 | 58 | | Tool | Purpose | Example Usage | 59 | |------|---------|---------------| 60 | | `mcp_xcf_xcf` | Execute XCF actions | `mcp_xcf_xcf(action="build")` | 61 | | `mcp_xcf_snippet` | Extract code snippets | `mcp_xcf_snippet(filePath="main.swift", entireFile=true)` | 62 | | `mcp_xcf_analyzer` | Analyze Swift code | `mcp_xcf_analyzer(filePath="main.swift", entireFile=true)` | 63 | | `mcp_xcf_read_file` | Read file contents | `mcp_xcf_read_file(filePath="main.swift")` | 64 | | `mcp_xcf_write_file` | Write file contents | `mcp_xcf_write_file(filePath="test.txt", content="Hello World")` | 65 | 66 | ### Standalone Action Tools 67 | 68 | In addition to the general-purpose tools, XCF now provides dedicated tools for each action: 69 | 70 | | Tool | Purpose | Example Usage | 71 | |------|---------|---------------| 72 | | `mcp_xcf_show_help` | Display help information | `mcp_xcf_show_help()` | 73 | | `mcp_xcf_grant_permission` | Grant Xcode permissions | `mcp_xcf_grant_permission()` | 74 | | `mcp_xcf_run_project` | Run the current project | `mcp_xcf_run_project()` | 75 | | `mcp_xcf_build_project` | Build the current project | `mcp_xcf_build_project()` | 76 | | `mcp_xcf_show_current_project` | Show selected project | `mcp_xcf_show_current_project()` | 77 | | `mcp_xcf_show_env` | Display environment variables | `mcp_xcf_show_env()` | 78 | | `mcp_xcf_show_folder` | Show current directory | `mcp_xcf_show_folder()` | 79 | | `mcp_xcf_list_projects` | List all Xcode projects | `mcp_xcf_list_projects()` | 80 | | `mcp_xcf_select_project` | Select a project by number | `mcp_xcf_select_project(projectNumber=1)` | 81 | | `mcp_xcf_analyze_swift_code` | Analyze Swift code | `mcp_xcf_analyze_swift_code(filePath="main.swift")` | 82 | 83 | ### Workflow Patterns 84 | 85 | #### Basic Project Management 86 | 87 | ``` 88 | # Activate XCF 89 | use_xcf 90 | 91 | # List and select project 92 | show 93 | open 1 94 | 95 | # Build and run 96 | build 97 | run 98 | ``` 99 | 100 | #### Code Analysis Workflow 101 | 102 | ``` 103 | # Activate XCF 104 | use_xcf 105 | 106 | # Get current project 107 | current 108 | 109 | # Extract and analyze code 110 | snippet main.swift 111 | analyzer main.swift 112 | 113 | # Implement fixes 114 | edit_file main.swift 10 20 "# Improved implementation" 115 | 116 | # Rebuild 117 | build 118 | ``` 119 | 120 | ## 🔍 Smart Path Resolution 121 | 122 | XCF uses intelligent path resolution for file operations: 123 | 124 | 1. Exact path provided 125 | 2. Relative path from current directory 126 | 3. Current project directory 127 | 4. Workspace folder 128 | 5. Recursive workspace search 129 | 6. Fuzzy filename matching 130 | 131 | ### File Access Examples 132 | 133 | ```python 134 | # These are equivalent 135 | mcp_xcf_snippet(filePath="/full/path/to/file.swift") 136 | mcp_xcf_snippet(filePath="file.swift") # Smart resolution 137 | ``` 138 | 139 | ## 📊 Code Analysis Capabilities 140 | 141 | The Swift code analyzer provides: 142 | - Code style checks 143 | - Complexity analysis 144 | - Unused variable detection 145 | - Magic number identification 146 | - Refactoring suggestions 147 | - Method length evaluation 148 | 149 | ### Analysis Example 150 | 151 | ```python 152 | # Analyze entire file 153 | mcp_xcf_analyzer(filePath="main.swift", entireFile=true) 154 | 155 | # Analyze specific line range 156 | mcp_xcf_analyzer( 157 | filePath="main.swift", 158 | startLine=10, 159 | endLine=50 160 | ) 161 | ``` 162 | 163 | ## 🔒 Security Considerations 164 | 165 | - Workspace-bound operations 166 | - Automatic access prevention outside workspace 167 | - Environment variable-based security 168 | - Safe redirection of potentially unsafe actions 169 | 170 | ## 🚀 Performance Tips 171 | 172 | - Use specific file paths when possible 173 | - Leverage smart path resolution 174 | - Utilize line-range analysis for large files 175 | - Prefer MCP function tools for programmatic interactions 176 | 177 | ## 🤝 Integration Best Practices 178 | 179 | 1. Always activate XCF before operations 180 | 2. Use environment variables for project configuration 181 | 3. Leverage smart path resolution 182 | 4. Implement error handling 183 | 5. Use code analysis before major changes 184 | 185 | ## 📋 Comprehensive Tool List 186 | 187 | Refer to the User Guide for a complete list of available tools and their detailed usage. 188 | 189 | ## 🎛️ XCF Core Actions 190 | 191 | ### Available Actions 192 | 193 | | Action | Description | Example | Purpose | 194 | |--------|-------------|---------|---------| 195 | | `grant` | Grant Xcode automation permissions | `mcp_xcf_xcf(action="grant")` | Authorize XCF to interact with Xcode | 196 | | `show` | List open projects | `mcp_xcf_xcf(action="show")` | Display available Xcode projects | 197 | | `open #` | Select project by number | `mcp_xcf_xcf(action="open 1")` | Choose a specific project to work on | 198 | | `current` | Show selected project | `mcp_xcf_xcf(action="current")` | Display the currently active project | 199 | | `build` | Build current project | `mcp_xcf_xcf(action="build")` | Compile the selected Xcode project | 200 | | `run` | Run current project | `mcp_xcf_xcf(action="run")` | Execute the current project | 201 | | `env` | Show environment variables | `mcp_xcf_xcf(action="env")` | Display system environment configuration | 202 | | `pwd` | Show current folder | `mcp_xcf_xcf(action="pwd")` | Display current working directory | 203 | | `analyze` | Analyze Swift code | `mcp_xcf_xcf(action="analyze main.swift")` | Perform code analysis on a file | 204 | | `lz` | Shorthand for analyze | `mcp_xcf_xcf(action="lz main.swift")` | Quick code analysis | 205 | 206 | ### Comprehensive Workflow Example 207 | 208 | ``` 209 | # Full project lifecycle management 210 | mcp_xcf_xcf(action="grant") # Authorize XCF 211 | mcp_xcf_xcf(action="show") # List projects 212 | mcp_xcf_xcf(action="open 1") # Select first project 213 | mcp_xcf_xcf(action="current") # Confirm project 214 | mcp_xcf_xcf(action="build") # Build project 215 | mcp_xcf_xcf(action="run") # Run project 216 | 217 | # Code analysis workflow 218 | mcp_xcf_xcf(action="analyze main.swift") # Detailed analysis 219 | mcp_xcf_xcf(action="lz main.swift") # Quick analysis 220 | ``` 221 | 222 | ### Environment and Context Management 223 | 224 | ```python 225 | # Check environment and context 226 | mcp_xcf_xcf(action="env") # Show environment variables 227 | mcp_xcf_xcf(action="pwd") # Show current working directory 228 | ``` 229 | 230 | ### Aliases and Shortcuts 231 | 232 | Some commands have convenient aliases: 233 | - `pwd` can also use `dir` or `path` 234 | - `lz` is a quick shorthand for `analyze` 235 | 236 | ## 🔍 Smart Action Resolution 237 | 238 | XCF intelligently handles actions by: 239 | - Automatically selecting the most recent project 240 | - Providing fallback mechanisms 241 | - Offering clear, actionable feedback 242 | - Supporting relative and absolute paths 243 | 244 | --- 245 | 246 | **Pro Tip:** Always use `grant` first to ensure full XCF functionality! 247 | 248 | --- 249 | 250 | Created by XCodeFreeze Automation - Empowering AI-driven Swift development! 251 | -------------------------------------------------------------------------------- /.cursor/rules/xcf-user-guide.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: xcf Xcode MCP Server - User Guide 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # XCF Xcode MCP Server - Comprehensive User Guide 🚀 7 | 8 | ## 🌟 Introduction 9 | 10 | XCF (Xcode MCP Server) is a powerful, Swift-native automation tool designed to revolutionize your Xcode development workflow. This guide provides an in-depth look at installation, configuration, and usage of XCF. 11 | 12 | ## 🔧 Installation & Setup 13 | 14 | ### System Requirements 15 | - macOS (latest version recommended) 16 | - Xcode installed 17 | - Basic understanding of terminal and Swift development 18 | 19 | ### Installation Steps 20 | 21 | 1. **Download XCF** 22 | - Visit the official website or GitHub repository 23 | - Download the latest XCF application 24 | 25 | 2. **Install Application** 26 | - Drag the XCF.app to your `/Applications` folder 27 | - Launch the application once to approve internet downloads 28 | 29 | 3. **Codesign (if needed)** 30 | ```bash 31 | codesign --force --deep --sign - /Applications/xcf.app 32 | ``` 33 | 34 | ### Configuration 35 | 36 | #### MCP Server Configuration 37 | 38 | Add XCF to your MCP configuration file: 39 | 40 | ```json 41 | { 42 | "mcpServers": { 43 | "xcf": { 44 | "type": "stdio", 45 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server" 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ##### Configuration Locations 52 | - **Cursor**: `~/.cursor/mcp.json` 53 | - **Claude Desktop**: `~/Library/Application Support/Claude/claude_desktop_config.json` 54 | 55 | #### Advanced Configuration 56 | 57 | For project-specific control: 58 | 59 | ```json 60 | { 61 | "mcpServers": { 62 | "xcf": { 63 | "type": "stdio", 64 | "command": "/Applications/xcf.app/Contents/MacOS/xcf server", 65 | "env": { 66 | "XCODE_PROJECT_FOLDER": "/path/to/project/", 67 | "XCODE_PROJECT": "/path/to/project/project.xcodeproj" 68 | } 69 | } 70 | } 71 | } 72 | ``` 73 | 74 | ## 🎬 Getting Started 75 | 76 | ### Activation 77 | 78 | Activate XCF mode: 79 | ``` 80 | use_xcf 81 | ``` 82 | 83 | ### Basic Workflow 84 | 85 | 1. List available projects 86 | ``` 87 | show 88 | ``` 89 | 90 | 2. Select a project 91 | ``` 92 | open 1 # Opens the first project in the list 93 | ``` 94 | 95 | 3. Build the project 96 | ``` 97 | build 98 | ``` 99 | 100 | 4. Run the project 101 | ``` 102 | run 103 | ``` 104 | 105 | ## 📂 File Operations 106 | 107 | ### Path Resolution and Quoting 108 | 109 | XCF supports flexible path resolution: 110 | - Current directory: `file.swift` 111 | - Child directories: `src/file.swift` 112 | - Parent directory: `../file.swift` 113 | - Multiple directories up: `../../file.swift` 114 | - Full system paths: `/Users/username/project/file.swift` 115 | 116 | **Quoting Rules:** 117 | - Use quotes for content with spaces 118 | - Can use single `'` or double `"` quotes 119 | - Recommended for complex content or paths with spaces 120 | 121 | ### Reading Files 122 | ```bash 123 | read_file main.swift # Current directory 124 | read_file src/utils.swift # Child directory 125 | read_file ../shared/config.swift # Parent directory 126 | read_file /full/path/to/main.swift # Full path 127 | ``` 128 | 129 | ### Writing Files 130 | ```bash 131 | write_file test.txt "Hello World" # Simple content 132 | write_file config.json '{"key": "value"}' # JSON content 133 | write_file "../shared/types.swift" 'import Foundation' # Quoted path 134 | ``` 135 | 136 | ### Editing Files 137 | ```bash 138 | edit_file main.swift 10 20 "Updated code" # Replace lines 10-20 139 | edit_file src/test.swift 5 5 "import UIKit" # Replace single line 140 | ``` 141 | 142 | ### File Management 143 | ```bash 144 | delete_file temp.txt # Delete file 145 | move_file old.swift new.swift # Move/rename file 146 | ``` 147 | 148 | ## 📁 Directory Operations 149 | 150 | ### Checking Current Directory 151 | ``` 152 | xcf pwd # Show current working directory 153 | cd_dir . # Confirm or reset to current directory 154 | ``` 155 | 156 | ### Changing Directories 157 | ``` 158 | cd_dir /path/to/project # Change to specific directory 159 | ``` 160 | 161 | ### Listing Contents 162 | ``` 163 | read_dir . # Current directory 164 | read_dir . swift # Swift files only 165 | ``` 166 | 167 | ### Directory Management 168 | - Create: `add_dir new_folder` 169 | - Remove: `rm_dir old_folder` 170 | - Move: `move_dir source_dir destination_dir` 171 | 172 | ### Best Practices 173 | 1. Always check current directory before file operations 174 | 2. Use `xcf pwd` to verify your working context 175 | 3. Use `cd_dir .` to ensure you're in the expected directory 176 | 177 | ## 📄 Xcode Document Management 178 | 179 | ### Opening Documents 180 | ```bash 181 | open_doc main.swift 182 | ``` 183 | 184 | ### Creating Documents 185 | ```bash 186 | create_doc new.swift "import Foundation" 187 | ``` 188 | 189 | ### Editing Documents 190 | ```bash 191 | 192 | ``` 193 | 194 | ### Document Lifecycle 195 | - Read: `read_doc main.swift` 196 | - Save: `save_doc main.swift` 197 | - Close: `close_doc main.swift true # With saving` 198 | 199 | ## 🔍 Code Analysis 200 | 201 | ### Snippet Extraction 202 | ```bash 203 | snippet main.swift # Entire file 204 | snippet main.swift 10 20 # Specific lines 205 | ``` 206 | 207 | ### Code Analysis 208 | ```bash 209 | analyzer main.swift # Full file analysis 210 | lz main.swift # Shorthand analysis 211 | ``` 212 | 213 | ### Analysis Features 214 | - Code style checks 215 | - Complexity evaluation 216 | - Unused variable detection 217 | - Refactoring suggestions 218 | - Method length analysis 219 | 220 | ## 🤖 AI Assistant Integration 221 | 222 | ### MCP Tools 223 | - `mcp_xcf_xcf`: Execute actions 224 | - `mcp_xcf_snippet`: Extract code 225 | - `mcp_xcf_analyzer`: Code analysis 226 | - `mcp_xcf_read_file`: Read files 227 | - `mcp_xcf_write_file`: Write files 228 | 229 | ### Standalone Action Tools 230 | XCF now provides direct tools for each action, making it easier to use and discover functionality: 231 | 232 | | Tool | Description | 233 | |------|-------------| 234 | | `mcp_xcf_show_help` | Display help information about available commands | 235 | | `mcp_xcf_grant_permission` | Grant Xcode automation permissions | 236 | | `mcp_xcf_run_project` | Run the current Xcode project | 237 | | `mcp_xcf_build_project` | Build the current Xcode project | 238 | | `mcp_xcf_show_current_project` | Show information about the currently selected project | 239 | | `mcp_xcf_show_env` | Display all environment variables | 240 | | `mcp_xcf_show_folder` | Display the current working folder | 241 | | `mcp_xcf_list_projects` | List all open Xcode projects | 242 | | `mcp_xcf_select_project` | Select an Xcode project by number | 243 | | `mcp_xcf_analyze_swift_code` | Analyze Swift code for potential issues | 244 | 245 | ### AI-Optimized Workflow Example 246 | ```python 247 | # Basic workflow using standalone tools 248 | mcp_xcf_use_xcf() # Activate XCF 249 | mcp_xcf_list_projects() # List available projects 250 | mcp_xcf_select_project(projectNumber=1) # Select first project 251 | mcp_xcf_build_project() # Build the project 252 | mcp_xcf_run_project() # Run the project 253 | 254 | # Code analysis using dedicated tools 255 | mcp_xcf_show_current_project() # Check current project 256 | mcp_xcf_analyze_swift_code(filePath="main.swift") # Analyze code 257 | ``` 258 | 259 | ## 🔒 Security & Best Practices 260 | 261 | ### Security Features 262 | - Workspace-bound operations 263 | - Automatic access prevention 264 | - Environment variable security 265 | - Safe action redirection 266 | 267 | ### Best Practices 268 | 1. Always activate XCF before operations 269 | 2. Use specific file paths 270 | 3. Leverage smart path resolution 271 | 4. Implement error handling 272 | 5. Analyze code before major changes 273 | 274 | ## 🚨 Troubleshooting 275 | 276 | ### Common Issues 277 | - Verify Xcode installation 278 | - Check XCF permissions 279 | - Validate configuration 280 | - Review environment variables 281 | 282 | ### Debugging Commands 283 | ```bash 284 | env # Show environment 285 | pwd # Current directory 286 | help # Available commands 287 | ``` 288 | 289 | ## 🌐 Community & Support 290 | 291 | - GitHub Repository: [XCF GitHub](mdc:https:/github.com/codefreezeai/xcf) 292 | - Website: [xcf.ai](mdc:https:/xcf.ai) 293 | - Community: Open to contributions! 294 | 295 | ## 📋 Version Information 296 | - Check current version with `xcf version` 297 | - Stay updated with latest releases 298 | 299 | --- 300 | 301 | Created by XCodeFreeze Automation - Empowering Swift Developers Worldwide! 🚀 302 | -------------------------------------------------------------------------------- /xcf/Mcp/File/XcfFuzzyLogicService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfFuzzyLogicService.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/4/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// A service that provides fuzzy matching and file finding capabilities 11 | struct FuzzyLogicService { 12 | // MARK: - Public Interface 13 | 14 | /// Expands a path handling both absolute paths and relative paths 15 | /// - Parameters: 16 | /// - path: The path to expand 17 | /// - projectDir: The project directory to use for relative paths 18 | /// - Returns: The expanded path 19 | static func expandPath(_ path: String, relativeTo projectDir: String) -> String { 20 | // For absolute paths 21 | if path.hasPrefix("/") || path.hasPrefix("~") { 22 | return (path as NSString).expandingTildeInPath 23 | } 24 | 25 | // For relative paths 26 | return (projectDir as NSString).appendingPathComponent(path) 27 | } 28 | 29 | /// Resolves any path (file or directory) using consistent rules 30 | /// - Parameter path: The path to resolve 31 | /// - Returns: A tuple containing the resolved path and any warning messages 32 | static func resolvePath(_ path: String, isDirectory: Bool = false) -> (path: String, warning: String) { 33 | // Get and verify the project directory 34 | guard let projectDir = XcfXcodeProjectManager.shared.currentFolder else { 35 | return (path, "No current project directory set") 36 | } 37 | 38 | // For "." or empty path, use project directory directly 39 | if path.isEmpty || path == "." { 40 | return (projectDir, "") 41 | } 42 | 43 | // For ".." path 44 | if path == ".." { 45 | return ((projectDir as NSString).deletingLastPathComponent, "") 46 | } 47 | 48 | // Expand the path 49 | let expandedPath = expandPath(path, relativeTo: projectDir) 50 | 51 | // Validate the path if needed 52 | var isDir: ObjCBool = ObjCBool(isDirectory) 53 | if FileManager.default.fileExists(atPath: expandedPath, isDirectory: &isDir) { 54 | if isDirectory == isDir.boolValue { 55 | return (expandedPath, "") 56 | } 57 | } 58 | 59 | // Even if file doesn't exist, return the expanded path for creation 60 | return (expandedPath, "") 61 | } 62 | 63 | /// Resolves a file path using consistent rules 64 | /// - Parameter filePath: The file path to resolve 65 | /// - Returns: A tuple containing the resolved path and any warning messages 66 | static func resolveFilePath(_ filePath: String) -> (path: String, warning: String) { 67 | return resolvePath(filePath, isDirectory: false) 68 | } 69 | 70 | /// Resolves a directory path using consistent rules 71 | /// - Parameter dirPath: The directory path to resolve 72 | /// - Returns: A tuple containing the resolved path and any warning messages 73 | static func resolveDirectoryPath(_ dirPath: String) -> (path: String, warning: String) { 74 | return resolvePath(dirPath, isDirectory: true) 75 | } 76 | 77 | // MARK: - Private Methods 78 | 79 | /// Search recursively in a directory for a file 80 | private static func searchRecursively(in directory: String, forFile filename: String, currentDepth: Int = 0, maxDepth: Int, duplicates: inout [String]) { 81 | guard currentDepth <= maxDepth else { return } 82 | 83 | let fileManager = FileManager.default 84 | guard let items = try? fileManager.contentsOfDirectory(atPath: directory) else { return } 85 | 86 | for item in items { 87 | let itemPath = (directory as NSString).appendingPathComponent(item) 88 | 89 | // Check if this is the file we're looking for 90 | if item == filename && fileManager.fileExists(atPath: itemPath) { 91 | duplicates.append(itemPath) 92 | } 93 | 94 | // If it's a directory, search recursively 95 | let isDirectory = (try? fileManager.attributesOfItem(atPath: itemPath)[.type] as? FileAttributeType) == .typeDirectory 96 | if isDirectory { 97 | searchRecursively(in: itemPath, forFile: filename, currentDepth: currentDepth + 1, maxDepth: maxDepth, duplicates: &duplicates) 98 | } 99 | } 100 | } 101 | 102 | /// Find the best fuzzy match for a filename across all available directories 103 | private static func findBestFuzzyMatch(for filename: String) -> (path: String, score: Double)? { 104 | var bestMatch: (path: String, score: Double)? = nil 105 | let directories = getSearchDirectories() 106 | 107 | for directory in directories { 108 | if let match = searchDirectoryForFuzzyMatch(filename, in: directory) { 109 | if bestMatch == nil || match.score > bestMatch!.score { 110 | bestMatch = match 111 | } 112 | } 113 | } 114 | 115 | // Only return matches above 70% similarity 116 | return bestMatch?.score ?? 0 > 0.7 ? bestMatch : nil 117 | } 118 | 119 | /// Get all directories to search in 120 | private static func getSearchDirectories() -> Set { 121 | var directories = Set() 122 | 123 | // Add current folder from project manager 124 | if let currentFolder = XcfXcodeProjectManager.shared.currentFolder { 125 | directories.insert(currentFolder) 126 | } 127 | 128 | // Add project directory 129 | if let projectPath = XcfXcodeProjectManager.shared.currentProject { 130 | let projectDir = (projectPath as NSString).deletingLastPathComponent 131 | directories.insert(projectDir) 132 | // Also add parent directory 133 | directories.insert((projectDir as NSString).deletingLastPathComponent) 134 | } 135 | 136 | return directories 137 | } 138 | 139 | /// Search a directory for fuzzy matches 140 | private static func searchDirectoryForFuzzyMatch(_ filename: String, in directory: String) -> (path: String, score: Double)? { 141 | var bestMatch: (path: String, score: Double)? = nil 142 | 143 | func searchDirectory(_ dir: String, maxDepth: Int = 2, currentDepth: Int = 0) { 144 | guard currentDepth <= maxDepth else { return } 145 | 146 | let fileManager = FileManager.default 147 | guard let items = try? fileManager.contentsOfDirectory(atPath: dir) else { return } 148 | 149 | for item in items { 150 | let itemPath = (dir as NSString).appendingPathComponent(item) 151 | 152 | // Check similarity for files 153 | let isDirectory = (try? fileManager.attributesOfItem(atPath: itemPath)[.type] as? FileAttributeType) == .typeDirectory 154 | if !isDirectory { 155 | let similarity = calculateStringSimilarity(filename, item) 156 | if bestMatch == nil || similarity > bestMatch!.score { 157 | bestMatch = (itemPath, similarity) 158 | } 159 | } else if currentDepth < maxDepth { 160 | // Recurse into subdirectories 161 | searchDirectory(itemPath, maxDepth: maxDepth, currentDepth: currentDepth + 1) 162 | } 163 | } 164 | } 165 | 166 | searchDirectory(directory) 167 | return bestMatch 168 | } 169 | 170 | /// Calculate string similarity using Levenshtein distance 171 | private static func calculateStringSimilarity(_ a: String, _ b: String) -> Double { 172 | let a = Array(a.lowercased()) 173 | let b = Array(b.lowercased()) 174 | 175 | let len_a = a.count 176 | let len_b = b.count 177 | 178 | // Special cases 179 | if len_a == 0 { return len_b == 0 ? 1.0 : 0.0 } 180 | if len_b == 0 { return 0.0 } 181 | 182 | // Initialize the distance matrix 183 | var matrix = [[Int]](repeating: [Int](repeating: 0, count: len_b + 1), count: len_a + 1) 184 | 185 | // Set the first row and column 186 | for i in 0...len_a { 187 | matrix[i][0] = i 188 | } 189 | for j in 0...len_b { 190 | matrix[0][j] = j 191 | } 192 | 193 | // Fill the matrix 194 | for i in 1...len_a { 195 | for j in 1...len_b { 196 | let cost = a[i-1] == b[j-1] ? 0 : 1 197 | matrix[i][j] = min( 198 | matrix[i-1][j] + 1, // deletion 199 | matrix[i][j-1] + 1, // insertion 200 | matrix[i-1][j-1] + cost // substitution 201 | ) 202 | } 203 | } 204 | 205 | // Calculate normalized similarity (1 - normalized distance) 206 | let maxLen = max(len_a, len_b) 207 | let distance = Double(matrix[len_a][len_b]) 208 | return 1.0 - (distance / Double(maxLen)) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /xcf/Mcp/File/XcfSecurityManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfSecurityManager.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/7/25. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Manages security checks for file operations 11 | class SecurityManager { 12 | // Singleton instance 13 | static let shared = SecurityManager() 14 | 15 | private init() {} 16 | 17 | /// The user's home directory 18 | private var homeDirectory: String { 19 | return FileManager.default.homeDirectoryForCurrentUser.path 20 | } 21 | 22 | /// The current working directory 23 | private var currentWorkingDirectory: String { 24 | return FileManager.default.currentDirectoryPath 25 | } 26 | 27 | /// Check if a path is within the user's home directory or current working directory 28 | /// - Parameter path: The path to check 29 | /// - Returns: A tuple containing (isAllowed, resolvedPath, errorMessage) 30 | func isPathAllowed(_ path: String) -> (allowed: Bool, resolvedPath: String, error: String?) { 31 | // First try to resolve the path using FileFinder for fuzzy matching 32 | let (resolvedPath, _) = FileFinder.resolveFilePath(path) 33 | let standardizedPath = (resolvedPath as NSString).standardizingPath 34 | 35 | // Always allow access to home directory itself 36 | if standardizedPath == homeDirectory { 37 | return (true, standardizedPath, nil) 38 | } 39 | 40 | // Check if path is within home directory 41 | if standardizedPath.hasPrefix(homeDirectory) { 42 | return (true, standardizedPath, nil) 43 | } 44 | 45 | // Check if it's a relative path or in current working directory 46 | let currentDir = currentWorkingDirectory 47 | if currentDir.hasPrefix(homeDirectory) { 48 | // If the current directory is within home directory, try relative path resolution 49 | let relativePath = (currentDir as NSString).appendingPathComponent(path) 50 | let standardizedRelativePath = (relativePath as NSString).standardizingPath 51 | 52 | if standardizedRelativePath.hasPrefix(homeDirectory) { 53 | return (true, standardizedRelativePath, nil) 54 | } 55 | 56 | // Try fuzzy matching within current directory 57 | if let fuzzyMatch = findFuzzyMatch(for: path, in: currentDir) { 58 | let standardizedFuzzyPath = (fuzzyMatch as NSString).standardizingPath 59 | if standardizedFuzzyPath.hasPrefix(homeDirectory) { 60 | return (true, standardizedFuzzyPath, nil) 61 | } 62 | } 63 | } 64 | 65 | return (false, standardizedPath, "Security Error: Access denied. Operations are restricted to your home directory (\(homeDirectory))") 66 | } 67 | 68 | /// Find a fuzzy match for a path in the specified directory 69 | /// - Parameters: 70 | /// - path: The path to find a match for 71 | /// - directory: The directory to search in 72 | /// - Returns: The matched path if found, nil otherwise 73 | private func findFuzzyMatch(for path: String, in directory: String) -> String? { 74 | let fileManager = FileManager.default 75 | guard let contents = try? fileManager.contentsOfDirectory(atPath: directory) else { 76 | return nil 77 | } 78 | 79 | // Clean up the path by trimming whitespace and normalizing separators 80 | let cleanPath = path.trimmingCharacters(in: .whitespaces) 81 | .replacingOccurrences(of: "\\", with: "/") 82 | let searchPath = (cleanPath as NSString).lastPathComponent 83 | 84 | // First try exact match (case-sensitive) 85 | if contents.contains(searchPath) { 86 | return (directory as NSString).appendingPathComponent(searchPath) 87 | } 88 | 89 | // Try case-insensitive exact match 90 | let lowercasePath = searchPath.lowercased() 91 | if let match = contents.first(where: { $0.lowercased() == lowercasePath }) { 92 | return (directory as NSString).appendingPathComponent(match) 93 | } 94 | 95 | // Try prefix match (case-insensitive) 96 | if let match = contents.first(where: { $0.lowercased().hasPrefix(lowercasePath) }) { 97 | return (directory as NSString).appendingPathComponent(match) 98 | } 99 | 100 | // Try contains match (case-insensitive) 101 | if let match = contents.first(where: { $0.lowercased().contains(lowercasePath) }) { 102 | return (directory as NSString).appendingPathComponent(match) 103 | } 104 | 105 | // Try matching by removing common file extensions 106 | let pathWithoutExt = (searchPath as NSString).deletingPathExtension.lowercased() 107 | if let match = contents.first(where: { 108 | let itemWithoutExt = ($0 as NSString).deletingPathExtension.lowercased() 109 | return itemWithoutExt == pathWithoutExt || 110 | itemWithoutExt.hasPrefix(pathWithoutExt) || 111 | itemWithoutExt.contains(pathWithoutExt) 112 | }) { 113 | return (directory as NSString).appendingPathComponent(match) 114 | } 115 | 116 | // Try Levenshtein distance for close matches 117 | let threshold = min(3, searchPath.count / 3) // Allow 1 error per 3 characters, max 3 118 | if threshold > 0 { 119 | var bestMatch: String? = nil 120 | var bestDistance = Int.max 121 | 122 | for item in contents { 123 | let itemWithoutExt = (item as NSString).deletingPathExtension.lowercased() 124 | let searchWithoutExt = pathWithoutExt 125 | 126 | let distance = levenshteinDistance(itemWithoutExt, searchWithoutExt) 127 | if distance < bestDistance && distance <= threshold { 128 | bestDistance = distance 129 | bestMatch = item 130 | } 131 | } 132 | 133 | if let match = bestMatch { 134 | return (directory as NSString).appendingPathComponent(match) 135 | } 136 | } 137 | 138 | return nil 139 | } 140 | 141 | /// Calculate Levenshtein distance between two strings 142 | private func levenshteinDistance(_ s1: String, _ s2: String) -> Int { 143 | let s1Array = Array(s1) 144 | let s2Array = Array(s2) 145 | let m = s1Array.count 146 | let n = s2Array.count 147 | 148 | var matrix = Array(repeating: Array(repeating: 0, count: n + 1), count: m + 1) 149 | 150 | // Initialize first row and column 151 | for i in 0...m { 152 | matrix[i][0] = i 153 | } 154 | for j in 0...n { 155 | matrix[0][j] = j 156 | } 157 | 158 | // Fill in the rest of the matrix 159 | for i in 1...m { 160 | for j in 1...n { 161 | if s1Array[i-1] == s2Array[j-1] { 162 | matrix[i][j] = matrix[i-1][j-1] 163 | } else { 164 | matrix[i][j] = min( 165 | matrix[i-1][j] + 1, // deletion 166 | matrix[i][j-1] + 1, // insertion 167 | matrix[i-1][j-1] + 1 // substitution 168 | ) 169 | } 170 | } 171 | } 172 | 173 | return matrix[m][n] 174 | } 175 | 176 | /// Check if a directory operation is allowed 177 | /// - Parameters: 178 | /// - path: The directory path to check 179 | /// - operation: The operation being performed (for error messages) 180 | /// - Returns: A tuple containing (isAllowed, resolvedPath, errorMessage) 181 | func isDirectoryOperationAllowed(_ path: String, operation: String) -> (allowed: Bool, resolvedPath: String, error: String?) { 182 | return isPathAllowed(path) 183 | } 184 | 185 | /// Check if a file operation is allowed 186 | /// - Parameters: 187 | /// - path: The file path to check 188 | /// - operation: The operation being performed (for error messages) 189 | /// - Returns: A tuple containing (isAllowed, resolvedPath, errorMessage) 190 | func isFileOperationAllowed(_ path: String, operation: String) -> (allowed: Bool, resolvedPath: String, error: String?) { 191 | return isPathAllowed(path) 192 | } 193 | 194 | /// Resolve and validate a path for security 195 | /// - Parameter path: The path to resolve and validate 196 | /// - Returns: A tuple containing (resolvedPath, errorMessage) 197 | func resolveAndValidatePath(_ path: String) -> (resolvedPath: String?, error: String?) { 198 | // First use FileFinder to resolve the path 199 | let (resolvedPath, _) = FileFinder.resolveFilePath(path) 200 | 201 | // Then check if the resolved path is allowed 202 | let (allowed, resolvedPath2, error) = isPathAllowed(resolvedPath) 203 | if !allowed { 204 | return (nil, error) 205 | } 206 | 207 | return (resolvedPath2, nil) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /xcf/Mcp/ServerConfig/XcfHelpText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfHelpText.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/18/25. 6 | // 7 | 8 | import Foundation 9 | 10 | // Define help text 11 | struct HelpText { 12 | // Quick help for xcf actions only 13 | static let basic = """ 14 | XCF Actions: 15 | grant - Grant Xcode automation permissions 16 | show - List open projects 17 | open # - Select project by number 18 | current - Show selected project 19 | build - Build current project 20 | run - Run current project 21 | env - Show environment variables 22 | pwd - Show current folder (aliases: dir, path) 23 | analyze - Analyze Swift code 24 | lz - Short for analyze 25 | 26 | Examples: 27 | xcf show 28 | xcf open 1 29 | xcf current 30 | xcf build 31 | xcf run 32 | xcf analyze main.swift 33 | xcf lz main.swift 34 | """ 35 | 36 | // Regular help with common examples 37 | static let detailed = """ 38 | Tool Commands: 39 | 40 | File Tools: 41 | read_file 42 | Read content from a file 43 | Example: read_file main.swift 44 | Example: read_file src/utils.swift 45 | Example: read_file ../shared/config.swift 46 | Example: read_file ../../other/project/file.swift 47 | Example: read_file /Users/username/Projects/app/main.swift 48 | 49 | write_file 50 | Write content to a file 51 | Example: write_file test.txt "Hello World" 52 | Example: write_file src/config.json '{"key": "value"}' 53 | Example: write_file ../shared/types.swift 'import Foundation' 54 | 55 | edit_file 56 | Edit specific lines in a file 57 | Example: edit_file main.swift 10 20 "new content" 58 | Example: edit_file src/test.swift 5 5 "import UIKit" 59 | Example: edit_file ../shared/types.swift 1 10 "new code" 60 | 61 | delete_file 62 | Delete a file 63 | Example: delete_file test.txt 64 | Example: delete_file src/old/backup.swift 65 | Example: delete_file ../backup/old.swift 66 | 67 | Directory Tools: 68 | cd_dir 69 | Change directory 70 | Example: cd_dir . 71 | Example: cd_dir src 72 | Example: cd_dir .. 73 | Example: cd_dir ../shared 74 | 75 | read_dir [path] [extension] 76 | List directory contents 77 | Example: read_dir . 78 | Example: read_dir src 79 | Example: read_dir ../shared 80 | Example: read_dir src swift 81 | 82 | add_dir 83 | Create directory 84 | Example: add_dir utils 85 | Example: add_dir src/models 86 | Example: add_dir ../shared/types 87 | 88 | rm_dir 89 | Remove directory 90 | Example: rm_dir temp 91 | Example: rm_dir src/cache 92 | Example: rm_dir ../old 93 | 94 | Xcode Tools: 95 | open_doc 96 | Open document in Xcode 97 | Example: open_doc main.swift 98 | Example: open_doc src/views/main.swift 99 | Example: open_doc ../shared/helpers.swift 100 | 101 | create_doc [content] 102 | Create new Xcode document 103 | Example: create_doc test.swift 104 | Example: create_doc src/models/user.swift 105 | Example: create_doc ../shared/types.swift 106 | 107 | read_doc 108 | Read Xcode document 109 | Example: read_doc main.swift 110 | Example: read_doc src/views/main.swift 111 | Example: read_doc ../shared/helpers.swift 112 | 113 | save_doc 114 | Save Xcode document 115 | Example: save_doc main.swift 116 | Example: save_doc src/views/main.swift 117 | Example: save_doc ../shared/helpers.swift 118 | 119 | 120 | 121 | Analysis Tools: 122 | snippet [start] [end] 123 | Extract code snippets 124 | Example: snippet main.swift 125 | Example: snippet src/utils.swift 126 | Example: snippet ../shared/types.swift 127 | Example: snippet main.swift 10 20 128 | 129 | analyzer [start] [end] 130 | Analyze Swift code 131 | Example: analyzer main.swift 132 | Example: analyzer src/models/user.swift 133 | Example: analyzer ../shared/types.swift 134 | Example: analyzer main.swift 10 20 135 | 136 | Notes: 137 | - All paths can be: 138 | - In current directory: file.swift 139 | - In child directories: src/file.swift 140 | - In parent directory: ../file.swift 141 | - Multiple directories up: ../../file.swift 142 | - Full system paths: /Users/username/project/file.swift 143 | - Content with spaces should be quoted 144 | - Use either ' or " for quoting content 145 | """ 146 | 147 | // Super detailed help with all tools and examples 148 | static let toolsReference = """ 149 | MCP Tool Reference 150 | 151 | Core Tools: 152 | xcf 153 | Description: Execute an xcf action or command 154 | Required Parameters: 155 | - action: string (The xcf action to execute) 156 | Example: mcp_xcf_xcf action="build" 157 | Example: mcp_xcf_xcf action="run" 158 | 159 | list 160 | Description: Lists all available tools on this server 161 | Parameters: none (type: string) 162 | Example: mcp_xcf_list 163 | 164 | xcf_help 165 | Description: Quick help for xcf commands 166 | Parameters: none (type: string) 167 | Example: mcp_xcf_xcf action="xcf_help" 168 | 169 | help 170 | Description: Regular help with common examples 171 | Parameters: none (type: string) 172 | Example: mcp_xcf_xcf action="help" 173 | 174 | File Operations: 175 | snippet 176 | Description: Extract code snippets from files in the current project 177 | Required Parameters: 178 | - filePath: string (Path to the file to extract snippet from) 179 | Optional Parameters: 180 | - startLine: integer (Starting line number, 1-indexed) 181 | - endLine: integer (Ending line number, 1-indexed) 182 | - entireFile: boolean (Set to true to get the entire file content) 183 | Example: mcp_xcf_snippet filePath="main.swift" entireFile=true 184 | Example: mcp_xcf_snippet filePath="main.swift" startLine=10 endLine=20 185 | 186 | analyzer 187 | Description: Analyze Swift code for potential issues 188 | Required Parameters: 189 | - filePath: string (Path to the file to extract snippet from) 190 | Optional Parameters: 191 | - startLine: integer (Starting line number, 1-indexed) 192 | - endLine: integer (Ending line number, 1-indexed) 193 | - entireFile: boolean (Set to true to get the entire file content) 194 | Example: mcp_xcf_analyzer filePath="main.swift" entireFile=true 195 | Example: mcp_xcf_analyzer filePath="main.swift" startLine=10 endLine=20 196 | 197 | read_dir 198 | Description: List contents of a directory 199 | Required Parameters: 200 | - directoryPath: string (Path to the directory) 201 | Optional Parameters: 202 | - fileExtension: string (Filter files by extension, e.g., 'swift') 203 | Example: mcp_xcf_read_dir directoryPath="src" 204 | Example: mcp_xcf_read_dir directoryPath="src" fileExtension="swift" 205 | 206 | write_file 207 | Description: Write content to a file 208 | Required Parameters: 209 | - filePath: string (Path to the file to write to) 210 | - content: string (Content to write to the file) 211 | Example: mcp_xcf_write_file filePath="main.swift" content="print(\"Hello\")" 212 | 213 | read_file 214 | Description: Read content from a file 215 | Required Parameters: 216 | - filePath: string (Path to the file to read from) 217 | Example: mcp_xcf_read_file filePath="main.swift" 218 | 219 | cd_dir 220 | Description: Change current directory 221 | Required Parameters: 222 | - directoryPath: string (Path to the directory) 223 | Example: mcp_xcf_cd_dir directoryPath="src" 224 | 225 | edit_file 226 | Description: Edit content in a file 227 | Required Parameters: 228 | - filePath: string (Path to the file to edit) 229 | - startLine: integer (Starting line number, 1-indexed) 230 | - endLine: integer (Ending line number, 1-indexed) 231 | - replacement: string (Replacement text for the specified lines) 232 | Example: mcp_xcf_edit_file filePath="main.swift" startLine=10 endLine=20 replacement="new code" 233 | 234 | delete_file 235 | Description: Delete a file 236 | Required Parameters: 237 | - filePath: string (Path to the file to delete) 238 | Example: mcp_xcf_delete_file filePath="old.swift" 239 | 240 | Directory Operations: 241 | add_dir 242 | Description: Create a new directory 243 | Required Parameters: 244 | - directoryPath: string (Path to the directory) 245 | Example: mcp_xcf_add_dir directoryPath="src/models" 246 | 247 | rm_dir 248 | Description: Remove a directory 249 | Required Parameters: 250 | - directoryPath: string (Path to the directory) 251 | Example: mcp_xcf_rm_dir directoryPath="old" 252 | 253 | Xcode Document Operations: 254 | open_doc 255 | Description: Open a document in Xcode 256 | Required Parameters: 257 | - filePath: string (Path to the file to open) 258 | Example: mcp_xcf_open_doc filePath="main.swift" 259 | 260 | create_doc 261 | Description: Create a new document in Xcode 262 | Required Parameters: 263 | - filePath: string (Path to the file to create) 264 | Optional Parameters: 265 | - content: string (Content to write to the file) 266 | Example: mcp_xcf_create_doc filePath="new.swift" 267 | Example: mcp_xcf_create_doc filePath="new.swift" content="import Foundation" 268 | 269 | read_doc 270 | Description: Read document content from Xcode 271 | Required Parameters: 272 | - filePath: string (Path to the file to read) 273 | Example: mcp_xcf_read_doc filePath="main.swift" 274 | 275 | save_doc 276 | Description: Save document in Xcode 277 | Required Parameters: 278 | - filePath: string (Path to the file to save) 279 | Example: mcp_xcf_save_doc filePath="main.swift" 280 | 281 | 282 | 283 | Mode Activation: 284 | use_xcf 285 | Description: Activate XCF mode 286 | Parameters: none (type: string) 287 | Example: mcp_xcf_use_xcf 288 | 289 | Parameter Types: 290 | - string: Text values, use quotes for values with spaces 291 | - integer: Whole numbers, 1-indexed for line numbers 292 | - boolean: true or false 293 | - object: Complex parameter type containing multiple values 294 | 295 | Notes: 296 | - All file paths can be: 297 | - Relative to current directory: main.swift 298 | - In subdirectories: src/main.swift 299 | - In parent directory: ../main.swift 300 | - Full system paths: /Users/username/project/main.swift 301 | - Line numbers are always 1-indexed 302 | - Content with spaces must be quoted 303 | - Boolean values are specified as true or false (lowercase) 304 | - All MCP calls start with mcp_xcf_ 305 | """ 306 | } 307 | -------------------------------------------------------------------------------- /xcf/Mcp/Prompts/XcfMcpPrompts.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MCP 3 | 4 | // MARK: - MCP Prompts 5 | 6 | class XcfMcpPrompts { 7 | 8 | static func getAllPrompts() -> [Prompt] { 9 | return [ 10 | createBuildPrompt(), 11 | createRunPrompt(), 12 | createAnalyzeCodePrompt(), 13 | createSnippetPrompt(), 14 | createReadFilePrompt(), 15 | createCdDirPrompt(), 16 | createOpenDocPrompt(), 17 | createCreateDocPrompt(), 18 | createReadDocPrompt(), 19 | createSaveDocPrompt(), 20 | createCloseDocPrompt(), 21 | createXcfActionPrompt(), 22 | createShowHelpPrompt(), 23 | createGrantPermissionPrompt(), 24 | createRunProjectPrompt(), 25 | createBuildProjectPrompt(), 26 | createShowCurrentProjectPrompt(), 27 | createShowEnvPrompt(), 28 | createShowFolderPrompt(), 29 | createListProjectsPrompt(), 30 | createSelectProjectPrompt(), 31 | createAnalyzeSwiftCodePrompt() 32 | ] 33 | } 34 | 35 | // MARK: - Prompt Creation Functions 36 | 37 | private static func createBuildPrompt() -> Prompt { 38 | Prompt( 39 | name: McpConfig.buildPromptName, 40 | description: McpConfig.buildPromptDesc, 41 | arguments: [ 42 | Prompt.Argument(name: McpConfig.projectPathArgName, description: McpConfig.projectPathArgDesc, required: true) 43 | ] 44 | ) 45 | } 46 | 47 | private static func createRunPrompt() -> Prompt { 48 | Prompt( 49 | name: McpConfig.runPromptName, 50 | description: McpConfig.runPromptDesc, 51 | arguments: [ 52 | Prompt.Argument(name: McpConfig.projectPathArgName, description: McpConfig.projectPathArgDesc, required: true) 53 | ] 54 | ) 55 | } 56 | 57 | private static func createAnalyzeCodePrompt() -> Prompt { 58 | Prompt( 59 | name: McpConfig.analyzeCodePromptName, 60 | description: McpConfig.analyzeCodePromptDesc, 61 | arguments: [ 62 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true), 63 | Prompt.Argument(name: McpConfig.includeSnippetArgName, description: McpConfig.includeSnippetArgDesc, required: false) 64 | ] 65 | ) 66 | } 67 | 68 | private static func createSnippetPrompt() -> Prompt { 69 | Prompt( 70 | name: "extractCodeSnippet", 71 | description: "Extract a code snippet from a file", 72 | arguments: [ 73 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true), 74 | Prompt.Argument(name: "startLine", description: "Starting line number", required: false), 75 | Prompt.Argument(name: "endLine", description: "Ending line number", required: false), 76 | Prompt.Argument(name: "entireFile", description: "Whether to extract the entire file", required: false) 77 | ] 78 | ) 79 | } 80 | 81 | private static func createReadFilePrompt() -> Prompt { 82 | Prompt( 83 | name: "readFile", 84 | description: "Read content from a file", 85 | arguments: [ 86 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true) 87 | ] 88 | ) 89 | } 90 | 91 | private static func createCdDirPrompt() -> Prompt { 92 | Prompt( 93 | name: "changeDirectory", 94 | description: "Change current directory", 95 | arguments: [ 96 | Prompt.Argument(name: "directoryPath", description: "Path to the directory", required: true) 97 | ] 98 | ) 99 | } 100 | 101 | private static func createOpenDocPrompt() -> Prompt { 102 | Prompt( 103 | name: "openDocument", 104 | description: "Open a document in Xcode", 105 | arguments: [ 106 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true) 107 | ] 108 | ) 109 | } 110 | 111 | private static func createCreateDocPrompt() -> Prompt { 112 | Prompt( 113 | name: "createDocument", 114 | description: "Create a new document in Xcode", 115 | arguments: [ 116 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true), 117 | Prompt.Argument(name: "content", description: "Initial content", required: false) 118 | ] 119 | ) 120 | } 121 | 122 | private static func createReadDocPrompt() -> Prompt { 123 | Prompt( 124 | name: "readDocument", 125 | description: "Read document content from Xcode", 126 | arguments: [ 127 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true) 128 | ] 129 | ) 130 | } 131 | 132 | private static func createSaveDocPrompt() -> Prompt { 133 | Prompt( 134 | name: "saveDocument", 135 | description: "Save document in Xcode", 136 | arguments: [ 137 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true) 138 | ] 139 | ) 140 | } 141 | 142 | private static func createCloseDocPrompt() -> Prompt { 143 | Prompt( 144 | name: "closeDocument", 145 | description: "Close a document in Xcode", 146 | arguments: [ 147 | Prompt.Argument(name: McpConfig.filePathArgName, description: McpConfig.filePathArgDesc, required: true), 148 | Prompt.Argument(name: "saving", description: "Whether to save before closing", required: true) 149 | ] 150 | ) 151 | } 152 | 153 | private static func createXcfActionPrompt() -> Prompt { 154 | Prompt( 155 | name: "executeXcfAction", 156 | description: "Execute an xcf action or command", 157 | arguments: [ 158 | Prompt.Argument(name: "action", description: "The xcf action to execute", required: true) 159 | ] 160 | ) 161 | } 162 | 163 | private static func createShowHelpPrompt() -> Prompt { 164 | Prompt( 165 | name: McpConfig.showHelpPromptName, 166 | description: McpConfig.showHelpToolDesc, 167 | arguments: [] 168 | ) 169 | } 170 | 171 | private static func createGrantPermissionPrompt() -> Prompt { 172 | Prompt( 173 | name: McpConfig.grantPermissionPromptName, 174 | description: McpConfig.grantPermissionToolDesc, 175 | arguments: [] 176 | ) 177 | } 178 | 179 | private static func createRunProjectPrompt() -> Prompt { 180 | Prompt( 181 | name: McpConfig.runProjectPromptName, 182 | description: McpConfig.runProjectToolDesc, 183 | arguments: [] 184 | ) 185 | } 186 | 187 | private static func createBuildProjectPrompt() -> Prompt { 188 | Prompt( 189 | name: McpConfig.buildProjectPromptName, 190 | description: McpConfig.buildProjectToolDesc, 191 | arguments: [] 192 | ) 193 | } 194 | 195 | private static func createShowCurrentProjectPrompt() -> Prompt { 196 | Prompt( 197 | name: McpConfig.showCurrentProjectPromptName, 198 | description: McpConfig.showCurrentProjectToolDesc, 199 | arguments: [] 200 | ) 201 | } 202 | 203 | private static func createShowEnvPrompt() -> Prompt { 204 | Prompt( 205 | name: McpConfig.showEnvPromptName, 206 | description: McpConfig.showEnvToolDesc, 207 | arguments: [] 208 | ) 209 | } 210 | 211 | private static func createShowFolderPrompt() -> Prompt { 212 | Prompt( 213 | name: McpConfig.showFolderPromptName, 214 | description: McpConfig.showFolderToolDesc, 215 | arguments: [] 216 | ) 217 | } 218 | 219 | private static func createListProjectsPrompt() -> Prompt { 220 | Prompt( 221 | name: McpConfig.listProjectsPromptName, 222 | description: McpConfig.listProjectsToolDesc, 223 | arguments: [] 224 | ) 225 | } 226 | 227 | private static func createSelectProjectPrompt() -> Prompt { 228 | Prompt( 229 | name: McpConfig.selectProjectPromptName, 230 | description: McpConfig.selectProjectToolDesc, 231 | arguments: [ 232 | Prompt.Argument(name: McpConfig.projectNumberParamName, description: McpConfig.projectNumberParamDesc, required: true) 233 | ] 234 | ) 235 | } 236 | 237 | private static func createAnalyzeSwiftCodePrompt() -> Prompt { 238 | Prompt( 239 | name: McpConfig.analyzeSwiftCodePromptName, 240 | description: McpConfig.analyzeSwiftCodeToolDesc, 241 | arguments: [ 242 | Prompt.Argument(name: McpConfig.filePathParamName, description: McpConfig.filePathParamDesc, required: true), 243 | Prompt.Argument(name: McpConfig.startLineParamName, description: McpConfig.startLineParamDesc, required: false), 244 | Prompt.Argument(name: McpConfig.endLineParamName, description: McpConfig.endLineParamDesc, required: false), 245 | Prompt.Argument(name: McpConfig.checkGroupsParamName, description: McpConfig.checkGroupsParamDesc, required: false) 246 | ] 247 | ) 248 | } 249 | } -------------------------------------------------------------------------------- /xcf/Mcp/Script/XcfSwiftScript.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XcfSwiftScript.swift 3 | // xcf 4 | // 5 | // Created by Todd Bruss on 5/7/25. 6 | // 7 | 8 | import AppKit 9 | import Foundation 10 | import ScriptingBridge 11 | 12 | class XcfSwiftScript { 13 | static let shared = XcfSwiftScript() 14 | private var files: Set = [] 15 | func buildCurrentWorkspace(projectPath: String, run: Bool = false) -> String { 16 | 17 | // Get Xcode application instance 18 | guard let xcode: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 19 | return ErrorMessages.failedToConnectXcode 20 | } 21 | 22 | // Open the project 23 | let xcDoc = xcode.open?(projectPath as Any) 24 | 25 | // Run the document 26 | guard 27 | let currentWorkspace = xcDoc as? XcodeWorkspaceDocument 28 | else { 29 | return ErrorMessages.noWorkspaceFound 30 | } 31 | 32 | currentWorkspace.stop?() 33 | 34 | var buildResult: XcodeSchemeActionResult? 35 | 36 | if run { 37 | sleep(XcodeConstants.runDelayInterval) // time for last one to get killed 38 | 39 | Task { 40 | currentWorkspace.runWithCommandLineArguments?(nil, withEnvironmentVariables: nil) 41 | } 42 | 43 | return SuccessMessages.runSuccess 44 | } else { 45 | buildResult = currentWorkspace.build?() 46 | if buildResult == nil { 47 | return ErrorMessages.failedToStartBuild 48 | } 49 | } 50 | 51 | // Now we can safely use buildResult outside the if/else blocks 52 | guard let result = buildResult else { 53 | return ErrorMessages.failedToGetBuildResult 54 | } 55 | 56 | // Wait for build to complete 57 | while !(result.completed ?? false) { 58 | Thread.sleep(forTimeInterval: XcodeConstants.buildPollInterval) 59 | } 60 | 61 | var buildResults = "" 62 | 63 | files = [] // Reset files collection before processing 64 | 65 | // Handle build errors with the original approach 66 | if let buildErrors = result.buildErrors?() { 67 | processIssues(issues: buildErrors, issueType: XcodeConstants.errorIssueType, buildResults: &buildResults) 68 | } 69 | 70 | // Handle other issue types if they're available 71 | if let buildWarnings = result.buildWarnings?() { 72 | processIssues(issues: buildWarnings, issueType: XcodeConstants.warningIssueType, buildResults: &buildResults) 73 | } 74 | 75 | if let analyzerIssues = result.analyzerIssues?() { 76 | processIssues(issues: analyzerIssues, issueType: XcodeConstants.analyzerIssueType, buildResults: &buildResults) 77 | } 78 | 79 | if let testFailures = result.testFailures?() { 80 | processIssues(issues: testFailures, issueType: XcodeConstants.testFailureIssueType, buildResults: &buildResults) 81 | } 82 | 83 | // Send entire file(s) at the end, uses a set to avoid duplicateso 84 | // Commented out: this uses too many tokens.. may send specific snippets later 85 | // for file in files { 86 | // buildResults += "\(XcodeConstants.filePrefix)\(file)\(XcodeConstants.fileSuffix)\(Format.newLine)" 87 | // let (fileContent, language) = CaptureSnippet.getCodeSnippet(filePath: file, startLine: 1, endLine: Int.max, entireFile: true) 88 | // buildResults += "\(XcodeConstants.codeBlockStart)\(language)\(Format.newLine)" 89 | // buildResults += fileContent 90 | // buildResults += "\(Format.newLine)\(XcodeConstants.codeBlockEnd)\(Format.newLine)" 91 | // } 92 | 93 | // Return build results 94 | return buildResults.isEmpty ? SuccessMessages.buildSuccess : buildResults 95 | } 96 | 97 | // Helper function to process different types of build issues 98 | private func processIssues(issues: SBElementArray, issueType: String, buildResults: inout String) { 99 | for case let issue as XcodeBuildError in issues { 100 | if let issueMessage = issue.message { 101 | if let filePath = issue.filePath, 102 | let startingColNum = issue.startingColumnNumber, 103 | let startLine = issue.startingLineNumber, 104 | let endLine = issue.endingLineNumber { 105 | files.insert(filePath) 106 | buildResults += String(format: XcodeConstants.issueFormat, 107 | filePath, startLine, startingColNum, issueType, issueMessage) + Format.newLine 108 | buildResults += formatCodeSnippet(filePath: filePath, startLine: startLine, endLine: endLine) 109 | } else { 110 | buildResults += "[\(issueType)] \(issueMessage)\(Format.newLine)" 111 | } 112 | } 113 | } 114 | } 115 | 116 | // Helper function to format code snippets consistently 117 | private func formatCodeSnippet(filePath: String, startLine: Int, endLine: Int) -> String { 118 | let (snippet, language) = CaptureSnippet.getCodeSnippet(filePath: filePath, startLine: startLine, endLine: endLine) 119 | var formattedSnippet = "\(XcodeConstants.codeBlockStart)\(language)\(Format.newLine)" 120 | formattedSnippet += snippet 121 | formattedSnippet += "\(Format.newLine)\(XcodeConstants.codeBlockEnd)\(Format.newLine)" 122 | return formattedSnippet 123 | } 124 | 125 | /// Gets paths of all open Xcode documents whose name contains the specified extension 126 | /// - Parameter ext: The extension or substring to filter document names by 127 | /// - Returns: Array of document paths matching the criteria, or empty array if none found or error occurred 128 | func getXcodeDocumentPaths(ext: String) -> [String] { 129 | // Get Xcode application instance 130 | guard let xcode: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 131 | return [] 132 | } 133 | 134 | // Get all documents 135 | guard let documents = xcode.documents?() else { 136 | return [] 137 | } 138 | 139 | var paths: Set = [] 140 | 141 | // Iterate through all documents and filter by extension 142 | for case let document as XcodeDocument in documents { 143 | if let name = document.name, name.contains(ext), let path = document.path { 144 | paths.insert(path) 145 | } 146 | } 147 | 148 | return Array(paths).sorted() 149 | } 150 | 151 | // New method to get schemes (not implemented) 152 | func getSchemes() -> [String] { 153 | // Get Xcode application instance 154 | guard let xcode: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 155 | return [] 156 | } 157 | 158 | // Get the active workspace document 159 | guard let activeWorkspace = xcode.activeWorkspaceDocument else { 160 | return [] 161 | } 162 | 163 | // Retrieve the schemes 164 | if let schemes = activeWorkspace.schemes?() as? [XcodeScheme] { 165 | return schemes.compactMap { $0.name } 166 | } else { 167 | return [] 168 | } 169 | } 170 | 171 | func activeWorkspacePath() -> String? { 172 | guard let xcode: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 173 | return nil 174 | } 175 | 176 | guard let activeWorkspace = xcode.activeWorkspaceDocument else { 177 | return nil 178 | } 179 | 180 | let projectPath = activeWorkspace.path 181 | 182 | // Only return the path if currentFolder exists and path contains currentFolder 183 | if let currentFolder = XcfXcodeProjectManager.shared.currentFolder { 184 | if let path = projectPath, path.contains(currentFolder) { 185 | return path 186 | } 187 | return nil 188 | } 189 | 190 | return projectPath 191 | } 192 | 193 | // MARK: - Swift Document Management 194 | 195 | /// Opens a Swift document in Xcode 196 | /// - Parameter filePath: The path to the Swift file to open 197 | /// - Returns: True if successful, false otherwise 198 | func openSwiftDocument(filePath: String) -> Bool { 199 | guard let xcode: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 200 | return false 201 | } 202 | 203 | // Use FileFinder to resolve the path 204 | let (resolvedPath, warning) = FileFinder.resolveFilePath(filePath) 205 | 206 | // First check if the file exists on disk using FileManager 207 | if !FileManager.default.fileExists(atPath: resolvedPath) { 208 | return false 209 | } 210 | 211 | // Check if the document is already open in Xcode using Scripting Bridge 212 | if let documents = xcode.documents?() { 213 | for case let document as XcodeDocument in documents { 214 | if let docPath = document.path, docPath == resolvedPath { 215 | return true 216 | } 217 | } 218 | } 219 | 220 | // Check if the file is accessible and readable 221 | guard FileManager.default.isReadableFile(atPath: resolvedPath) else { 222 | return false 223 | } 224 | 225 | // Try to open the document in Xcode using Scripting Bridge 226 | guard let openedDocument = xcode.open?(resolvedPath as Any) as? XcodeTextDocument else { 227 | return false 228 | } 229 | 230 | // Verify the document was actually opened by checking its path 231 | if let openedPath = openedDocument.path, openedPath == resolvedPath { 232 | return true 233 | } else { 234 | return false 235 | } 236 | } 237 | 238 | /// Creates a new Swift document using Xcode ScriptingBridge 239 | /// - Parameters: 240 | /// - filePath: The path where the Swift file should be created 241 | /// - content: Initial content of the Swift file (optional) 242 | /// - Returns: True if successful, false otherwise 243 | func createSwiftDocumentWithFileManager(filePath: String, content: String = "") -> Bool { 244 | guard let _: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 245 | return false 246 | } 247 | 248 | // Use FileFinder to resolve the path 249 | let (resolvedPath, warning) = FileFinder.resolveFilePath(filePath) 250 | 251 | // First create the file using FileManager 252 | do { 253 | try XcfFileManager.writeFile(content: content, to: resolvedPath) 254 | } catch { 255 | return false 256 | } 257 | 258 | // Try to open the document 259 | return openSwiftDocument(filePath: resolvedPath) 260 | } 261 | 262 | // MARK: - Document Reading 263 | 264 | /// Reads the contents of a Swift document using FileManager 265 | /// - Parameter filePath: The path to the Swift file to read 266 | /// - Returns: The file contents as a string, or nil if the operation failed 267 | func readSwiftDocumentWithFileManager(filePath: String) -> String? { 268 | do { 269 | let (content, _) = try XcfFileManager.readFile(at: filePath) 270 | return content 271 | } catch { 272 | return nil 273 | } 274 | } 275 | 276 | // MARK: - Document Writing 277 | 278 | /// Writes content to a Swift document using FileManager 279 | /// - Parameters: 280 | /// - filePath: The path to the Swift file to write to 281 | /// - content: The content to write to the file 282 | /// - Returns: True if successful, false otherwise 283 | func writeSwiftDocumentWithFileManager(filePath: String, content: String) -> Bool { 284 | do { 285 | try XcfFileManager.writeFile(content: content, to: filePath) 286 | return true 287 | } catch { 288 | return false 289 | } 290 | } 291 | 292 | // MARK: - Directory Operations 293 | /// Shows a directory selection dialog and returns the selected path 294 | /// - Returns: The selected directory path, or nil if cancelled 295 | func selectDirectory() -> String? { 296 | return XcfFileManager.selectDirectory() 297 | } 298 | 299 | /// Closes a Swift document in Xcode 300 | /// - Parameter filePath: The path to the Swift file to close 301 | /// - Returns: True if successful, false otherwise 302 | func closeSwiftDocument(filePath: String, xcSaveOptions: XcodeSaveOptions ) -> Bool { 303 | guard let xcode: XcodeApplication = SBApplication(bundleIdentifier: XcodeConstants.xcodeBundleIdentifier) else { 304 | return false 305 | } 306 | 307 | // Use FileFinder to resolve the path 308 | let (resolvedPath, warning) = FuzzyLogicService.resolveFilePath(filePath) 309 | 310 | // Find the document in Xcode 311 | guard let documents = xcode.documents?() else { 312 | return false 313 | } 314 | 315 | // Try to find document with exact path 316 | if let document = (documents as? [XcodeDocument])?.first(where: { $0.path == resolvedPath }) { 317 | // Close the document 318 | document.closeSaving?(xcSaveOptions, savingIn: nil) 319 | return true 320 | } 321 | 322 | // Try to find document with filename match (more lenient) 323 | let filename = (resolvedPath as NSString).lastPathComponent 324 | if let document = (documents as? [XcodeDocument])?.first(where: { 325 | if let docPath = $0.path { 326 | return (docPath as NSString).lastPathComponent == filename 327 | } 328 | return false 329 | }) { 330 | // Close the document 331 | document.closeSaving?(xcSaveOptions, savingIn: nil) 332 | return true 333 | } 334 | 335 | return false 336 | } 337 | } 338 | --------------------------------------------------------------------------------